Unverified Commit c32bd049 authored by Jörg Thalheim's avatar Jörg Thalheim Committed by GitHub
Browse files

signal-desktop: replace unlicensed Apple emoji (#337161)

parents 1e359fcf 98648422
Loading
Loading
Loading
Loading
+118 −0
Original line number Diff line number Diff line
"""Copy Noto Color Emoji PNGs into an extracted Signal ASAR archive.

Signal loads small Apple emoji PNGs directly from
`node_modules/emoji-datasource-apple/img/apple/64`, and downloads and
caches large Apple emoji WebP files in `.proto` bundles on the fly. The
latter are not a copyright concern for the Nixpkgs cache, but would
result in inconsistent presentation between small and large emoji.

We skip the complexity and buy some additional privacy by replacing the
`emoji://jumbo?emoji=` URL prefix with a `file://` path to the copied
PNGs inside the ASAR archive, and linking the `node_modules` PNG paths
directly to them.
"""

import json
import shutil
import sys
from pathlib import Path


def signal_name_to_emoji(signal_emoji_name: str) -> str:
    r"""Return the emoji corresponding to a Signal emoji name.

    Signal emoji names are concatenations of UTF‐16 code units,
    represented in lowercase big‐endian hex padded to four digits.

    >>> signal_name_to_emoji("d83dde36200dd83cdf2bfe0f")
    '😶‍🌫️'
    >>> b"\xd8\x3d\xde\x36\x20\x0d\xd8\x3c\xdf\x2b\xfe\x0f".decode("utf-16-be")
    '😶‍🌫️'
    """
    hex_bytes = zip(signal_emoji_name[::2], signal_emoji_name[1::2])
    emoji_utf_16_be = bytes(
        int("".join(hex_pair), 16) for hex_pair in hex_bytes
    )
    return emoji_utf_16_be.decode("utf-16-be")


def emoji_to_noto_name(emoji: str) -> str:
    r"""Return the Noto emoji name of an emoji.

    Noto emoji names are underscore‐separated Unicode scalar values,
    represented in lowercase big‐endian hex padded to at least four
    digits. Any U+FE0F variant selectors are omitted.

    >>> emoji_to_noto_name("😶‍🌫️")
    '1f636_200d_1f32b'
    >>> emoji_to_noto_name("\U0001f636\u200d\U0001f32b\ufe0f")
    '1f636_200d_1f32b'
    """
    return "_".join(
        f"{ord(scalar_value):04x}"
        for scalar_value in emoji
        if scalar_value != "\ufe0f"
    )


def emoji_to_emoji_data_name(emoji: str) -> str:
    r"""Return the npm emoji-data emoji name of an emoji.

    emoji-data emoji names are hyphen‐minus‐separated Unicode scalar
    values, represented in lowercase big‐endian hex padded to at least
    four digits.

    >>> emoji_to_emoji_data_name("😶‍🌫️")
    '1f636-200d-1f32b-fe0f'
    >>> emoji_to_emoji_data_name("\U0001f636\u200d\U0001f32b\ufe0f")
    '1f636-200d-1f32b-fe0f'
    """
    return "-".join(f"{ord(scalar_value):04x}" for scalar_value in emoji)


def _main() -> None:
    noto_png_path, asar_root = (Path(arg) for arg in sys.argv[1:])
    asar_root = asar_root.absolute()

    out_path = asar_root / "images" / "nixpkgs-emoji"
    out_path.mkdir(parents=True)

    emoji_data_out_path = (
        asar_root
        / "node_modules"
        / "emoji-datasource-apple"
        / "img"
        / "apple"
        / "64"
    )
    emoji_data_out_path.mkdir(parents=True)

    jumbomoji_json_path = asar_root / "build" / "jumbomoji.json"
    with jumbomoji_json_path.open() as jumbomoji_json_file:
        jumbomoji_packs = json.load(jumbomoji_json_file)

    for signal_emoji_names in jumbomoji_packs.values():
        for signal_emoji_name in signal_emoji_names:
            emoji = signal_name_to_emoji(signal_emoji_name)

            try:
                shutil.copy(
                    noto_png_path / f"emoji_u{emoji_to_noto_name(emoji)}.png",
                    out_path / emoji,
                )
            except FileNotFoundError:
                print(
                    f"Missing Noto emoji: {emoji} {signal_emoji_name}",
                    file=sys.stderr,
                )
                continue

            (
                emoji_data_out_path / f"{emoji_to_emoji_data_name(emoji)}.png"
            ).symlink_to(out_path / emoji)

    print(out_path.relative_to(asar_root))


if __name__ == "__main__":
    _main()
+95 −5
Original line number Diff line number Diff line
{ stdenv
, lib
, callPackage
, fetchurl
, autoPatchelfHook
, noto-fonts-color-emoji
, dpkg
, asar
, rsync
, python3
, wrapGAppsHook3
, makeWrapper
, nixosTests
@@ -57,6 +62,27 @@
let
  inherit (stdenv) targetPlatform;
  ARCH = if targetPlatform.isAarch64 then "arm64" else "x64";

  # Noto Color Emoji PNG files for emoji replacement; see below.
  noto-fonts-color-emoji-png = noto-fonts-color-emoji.overrideAttrs (prevAttrs: {
    pname = "noto-fonts-color-emoji-png";

    # The build produces 136×128 PNGs by default for arcane font
    # reasons, but we want square PNGs.
    buildFlags = prevAttrs.buildFlags or [ ] ++ [ "BODY_DIMENSIONS=128x128" ];

    makeTargets = [ "compressed" ];

    installPhase = ''
      runHook preInstall

      mkdir -p $out/share
      mv build/compressed_pngs $out/share/noto-fonts-color-emoji-png
      python3 add_aliases.py --srcdir=$out/share/noto-fonts-color-emoji-png

      runHook postInstall
    '';
  });
in
stdenv.mkDerivation rec {
  inherit pname version;
@@ -71,11 +97,36 @@ stdenv.mkDerivation rec {

  src = fetchurl {
    inherit url hash;
    recursiveHash = true;
    downloadToTemp = true;
    nativeBuildInputs = [ dpkg asar ];
    # Signal ships the Apple emoji set without a licence via an npm
    # package and upstream does not seem terribly interested in fixing
    # this; see:
    #
    # * <https://github.com/signalapp/Signal-Android/issues/5862>
    # * <https://whispersystems.discoursehosting.net/t/signal-is-likely-violating-apple-license-terms-by-using-apple-emoji-in-the-sticker-creator-and-android-and-desktop-apps/52883>
    #
    # We work around this by replacing it with the Noto Color Emoji
    # set, which is available under a FOSS licence and more likely to
    # be used on a NixOS machine anyway. The Apple emoji are removed
    # during `fetchurl` to ensure that the build doesn’t cache the
    # unlicensed emoji files, but the rest of the work is done in the
    # main derivation.
    postFetch = ''
      dpkg-deb -x $downloadedFile $out
      asar extract "$out/opt/${dir}/resources/app.asar" $out/asar-contents
      rm -r \
        "$out/opt/${dir}/resources/app.asar"{,.unpacked} \
        $out/asar-contents/node_modules/emoji-datasource-apple
    '';
  };

  nativeBuildInputs = [
    rsync
    asar
    python3
    autoPatchelfHook
    dpkg
    (wrapGAppsHook3.override { inherit makeWrapper; })
  ];

@@ -127,11 +178,13 @@ stdenv.mkDerivation rec {
    wayland
  ];

  unpackPhase = "dpkg-deb -x $src .";

  dontBuild = true;
  dontConfigure = true;

  unpackPhase = ''
    rsync -a --chmod=+w $src/ .
  '';

  installPhase = ''
    runHook preInstall

@@ -147,6 +200,30 @@ stdenv.mkDerivation rec {
    # Create required symlinks:
    ln -s libGLESv2.so "$out/lib/${dir}/libGLESv2.so.2"

    # Copy the Noto Color Emoji PNGs into the ASAR contents. See `src`
    # for the motivation, and the script for the technical details.
    emojiPrefix=$(
      python3 ${./copy-noto-emoji.py} \
      ${noto-fonts-color-emoji-png}/share/noto-fonts-color-emoji-png \
      asar-contents
    )

    # Replace the URL used for fetching large versions of emoji with
    # the local path to our copied PNGs.
    substituteInPlace asar-contents/preload.bundle.js \
      --replace-fail \
        'emoji://jumbo?emoji=' \
        "file://$out/lib/${lib.escapeURL dir}/resources/app.asar/$emojiPrefix/"

    # `asar(1)` copies files from the corresponding `.unpacked`
    # directory when extracting, and will put them back in the modified
    # archive if you don’t specify them again when repacking. Signal
    # leaves their native `.node` libraries unpacked, so we match that.
    asar pack \
      --unpack '*.node' \
      asar-contents \
      "$out/lib/${dir}/resources/app.asar"

    runHook postInstall
  '';

@@ -180,8 +257,21 @@ stdenv.mkDerivation rec {
    '';
    homepage = "https://signal.org/";
    changelog = "https://github.com/signalapp/Signal-Desktop/releases/tag/v${version}";
    license = lib.licenses.agpl3Only;
    maintainers = with lib.maintainers; [ eclairevoyant mic92 equirosa urandom bkchr teutat3s ];
    license = [
      lib.licenses.agpl3Only

      # Various npm packages
      lib.licenses.free
    ];
    maintainers = with lib.maintainers; [
      eclairevoyant
      mic92
      equirosa
      urandom
      bkchr
      teutat3s
      emily
    ];
    mainProgram = pname;
    platforms = [ "x86_64-linux" "aarch64-linux" ];
    sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ];
+15 −0
Original line number Diff line number Diff line
[tool.mypy]
files = ["*.py"]
strict = true

[tool.ruff]
line-length = 80

[tool.ruff.lint]
select = ["ALL"]
ignore = ["COM812", "D203", "D213", "ISC001", "T201"]
allowed-confusables = ["‐"]

[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = "dynamic"
+1 −1
Original line number Diff line number Diff line
@@ -4,5 +4,5 @@ callPackage ./generic.nix { } rec {
  dir = "Signal";
  version = "7.19.0";
  url = "https://github.com/0mniteck/Signal-Desktop-Mobian/raw/${version}/builds/release/signal-desktop_${version}_arm64.deb";
  hash = "sha256-L5Wj1ofMR+QJezd4V6pAhkINLF6y9EB5VNFAIOZE5PU=";
  hash = "sha256-wyXVZUuY1TDGAVq7Gx9r/cuBuoMmSk9KQttTJlIN+k8=";
}
+1 −1
Original line number Diff line number Diff line
@@ -4,5 +4,5 @@ callPackage ./generic.nix { } rec {
  dir = "Signal Beta";
  version = "7.19.0-beta.1";
  url = "https://updates.signal.org/desktop/apt/pool/s/signal-desktop-beta/signal-desktop-beta_${version}_amd64.deb";
  hash = "sha256-kD08xke+HYhwAZG7jmU1ILo013556vNcvAFc/+9BTjg=";
  hash = "sha256-dIZvzJ45c5kL+2HEaKrtbck5Zz572pQAj3YTenzz6Zs=";
}
Loading