File wechat-universal.sh of Package wechat-universal-bwrap

#!/bin/bash
#
# wechat-universal.sh - wrapper for WeChat Universal
# Copyright (C) 2024 Guoxin "7Ji" Pu <pugokushin@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#

dbus_call() { # $1: dest, $2: object path, $3: method
    local args=(dbus-send --session --dest="$1" --print-reply "${@:2}")
    echo "D-Bus calling with: ${args[@]}" >&2
    "${args[@]}"
}

# Based on: https://github.com/web1n/wechat-universal-flatpak/blob/91f5d0d246881077812881e800a22f5a02cea438/wechat.sh#L3
notifier_get() {
    local dbus_item
    for dbus_item in $(
        dbus_call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.ListNames |
            sed -n 's/      string "\(org\.kde\.StatusNotifierItem.\+\)"/\1/p'
    )
    do
        if dbus_call "${dbus_item}" \
                /StatusNotifierItem \
                org.freedesktop.DBus.Properties.Get \
                string:org.kde.StatusNotifierItem \
                string:Id |
            grep -q 'string "wechat"'
        then
            echo "${dbus_item}"
            return
        fi
    done
}

try_move_foreground() {
    # Try to find existing WeChat window first and display it, before actually opening a new one
    notifier_item=$(notifier_get)
    if [[ "${notifier_item}" ]]; then
        echo 'WeChat is already running, moving it to foreground'
        # Based on: https://github.com/web1n/wechat-universal-flatpak/blob/91f5d0d246881077812881e800a22f5a02cea438/wechat.sh#L25
        dbus_call "${notifier_item}" \
            /StatusNotifierItem \
            org.kde.StatusNotifierItem.Activate \
            int32:0 \
            int32:0
        return $?
    fi
    return 1
}

try_start() {
    # Data folder setup
    # If user has declared a custom data dir, no need to query xdg for documents dir, but always resolve that to absolute path
    if [[ "${WECHAT_DATA_DIR}" ]]; then
        WECHAT_DATA_DIR=$(readlink -f -- "${WECHAT_DATA_DIR}")
    else
        XDG_DOCUMENTS_DIR="${XDG_DOCUMENTS_DIR:-$(xdg-user-dir DOCUMENTS)}"
        if [[ -z "${XDG_DOCUMENTS_DIR}" ]]; then
            echo 'Error: Failed to get XDG_DOCUMENTS_DIR, refuse to continue'
            exit 1
        fi
        WECHAT_DATA_DIR="${XDG_DOCUMENTS_DIR}/WeChat_Data"
    fi
    echo "Using '${WECHAT_DATA_DIR}' as Wechat Data folder"
    WECHAT_FILES_DIR="${WECHAT_DATA_DIR}/xwechat_files"
    WECHAT_HOME_DIR="${WECHAT_DATA_DIR}/home"

    # Runtime folder setup
    XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-$(xdg-user-dir RUNTIME)}"
    if [[ -z "${XDG_RUNTIME_DIR}" ]]; then
        echo 'Error: Failed to get XDG_RUNTIME_DIR, refuse to continue'
        exit 1
    fi
    if [[ -z "${XAUTHORITY}" ]]; then
        echo 'Warning: No XAUTHORITY set, runnning in no-X environment? Generating it'
        export XAUTHORITY=$(mktemp "${XDG_RUNTIME_DIR}"/xauth_XXXXXX)
        echo "Info: Generated XAUTHORITY at '${XAUTHORITY}'"
    fi

    # wechat-universal only supports xcb
    export QT_QPA_PLATFORM=xcb \
        QT_AUTO_SCREEN_SCALE_FACTOR=1 \
        PATH="/sandbox:${PATH}"

    if [[ -z "${WECHAT_IME_WORKAROUND}" || "${WECHAT_IME_WORKAROUND}" == 'auto' ]]; then
        case "${XMODIFIERS}" in 
            *@im=fcitx*)
                WECHAT_IME_WORKAROUND='fcitx'
                ;;
            *@im=ibus*)
                WECHAT_IME_WORKAROUND='ibus'
                ;;
        esac
    fi

    case "${WECHAT_IME_WORKAROUND}" in
        fcitx)
            echo "IME workaround for fcitx applied"
            export QT_IM_MODULE=fcitx \
                GTK_IM_MODULE=fcitx
            ;;
        ibus)
            echo "IME workaround for ibus applied"
            export QT_IM_MODULE=ibus \
                GTK_IM_MODULE=ibus \
                IBUS_USE_PORTAL=1
            ;;
    esac

    BWRAP_DEV_BINDS=()
    for DEV_NODE in /dev/{nvidia{-uvm,ctl,*[0-9]},video*[0-9]}; do
        [[ -e "${DEV_NODE}" ]] && BWRAP_DEV_BINDS+=(--dev-bind "${DEV_NODE}"{,})
    done

    # Custom exposed folders
    # echo "Hint: Custom binds could either be declared in '~/.config/wechat-universal/binds.list', each line a path, absolute or relative to your HOME; or as argument \`--bind [custom bind]\`"
    BWRAP_CUSTOM_BINDS=()
    if [[ -f "${WECHAT_CUSTOM_BINDS_CONFIG}" ]]; then
        mapfile -t WECHAT_CUSTOM_BINDS_PRESISTENT < "${WECHAT_CUSTOM_BINDS_CONFIG}"
    fi
    cd ~ # 7Ji: two chdir.3 should be cheaper than a lot of per-dir calculation
    for WECHAT_CUSTOM_BIND in  "${WECHAT_CUSTOM_BINDS_RUNTIME[@]}" "${WECHAT_CUSTOM_BINDS_PRESISTENT[@]}"; do
        WECHAT_CUSTOM_BIND=$(readlink -f -- "${WECHAT_CUSTOM_BIND}")
        echo "Custom bind: '${WECHAT_CUSTOM_BIND}'"
        BWRAP_CUSTOM_BINDS+=(--bind "${WECHAT_CUSTOM_BIND}"{,})
    done
    cd - > /dev/null

    DBUS_SESSION_BUS_PATH=''
    if [[ "${DBUS_SESSION_BUS_ADDRESS}" ]]; then
        DBUS_SESSION_BUS_PATH=$(echo "${DBUS_SESSION_BUS_ADDRESS}" | sed 's/^unix:\(.\+=.\+\)\?path=\(.\+\)\(,.\+=.\+\|$\)/\2/')
    fi
    if [[ -z "${DBUS_SESSION_BUS_PATH}" ]]; then
        DBUS_SESSION_BUS_PATH="${XDG_RUNTIME_DIR}/bus"
        echo "Failed to extract \$DBUS_SESSION_BUS_ADDRESS from \$DBUS_SESSION_BUS_ADDRESS (${DBUS_SESSION_BUS_ADDRESS}), using fallback ${DBUS_SESSION_BUS_PATH}"
    fi

    mkdir -p "${WECHAT_FILES_DIR}" "${WECHAT_HOME_DIR}"
    ln -snf "${WECHAT_FILES_DIR}" "${WECHAT_HOME_DIR}/xwechat_files"

    if [[ "${WECHAT_MULTIPLE_INSTANCE}" ]];then
        BWRAP_ARGS=(
            --unshare-user-try
            --unshare-ipc
            --unshare-uts
            --unshare-cgroup-try
    )
    else
        BWRAP_ARGS=(
        # Drop privileges
        --unshare-all
    )
    fi

    BWRAP_ARGS+=(
        --share-net
        --cap-drop ALL
        --die-with-parent

        # /
        --ro-bind /lib{,}
        --ro-bind /lib64{,}
        --ro-bind /bin{,}

        # /usr
        --ro-bind /usr{,}
        --bind /usr/bin/{true,lsblk}

        # /sandbox
        --ro-bind /usr/bin/flatpak-xdg-open /sandbox/xdg-open
        --ro-bind INSTALL_RUNENV/usr/bin/common.sh /sandbox/dde-file-manager

        # /dev
        --dev /dev
        --dev-bind /dev/dri{,}
        --tmpfs /dev/shm

        # /proc
        --proc /proc

        # /etc
        --ro-bind /etc/machine-id{,}
        --ro-bind /etc/passwd{,}
        --ro-bind /etc/nsswitch.conf{,}
        --ro-bind /etc/resolv.conf{,}
        --ro-bind /etc/localtime{,}
        --ro-bind-try /etc/fonts{,}
        --ro-bind-try /etc/alternatives/awk{,}

        # /sys
        --dir /sys/dev # hack for Intel / AMD graphics, mesa calling virtual nodes needs /sys/dev being 0755
        --ro-bind /sys/dev/char{,}
        --ro-bind /sys/devices{,}

        # /tmp
        --tmpfs /tmp

        # /install root dir
        --ro-bind INSTALL_ROOTDIR{,}

        # /home
        --bind "${WECHAT_HOME_DIR}" "${HOME}"
        --bind "${WECHAT_FILES_DIR}"{,}
        --bind-try "${HOME}/.pki"{,}
        --ro-bind-try "${HOME}/.fontconfig"{,}
        --ro-bind-try "${HOME}/.fonts"{,}
        --ro-bind-try "${HOME}/.config/fontconfig"{,}
        --ro-bind-try "${HOME}/.local/share/fonts"{,}
        --ro-bind-try "${HOME}/.icons"{,}
        --ro-bind-try "${HOME}/.local/share/icons"{,}

        # /run
        --dev-bind /run/dbus{,}
        --ro-bind-try /run/systemd/userdb{,}
        --ro-bind-try "${XAUTHORITY}"{,}
        --ro-bind "${DBUS_SESSION_BUS_PATH}"{,}
        --ro-bind "${XDG_RUNTIME_DIR}/pulse"{,}
    )

    case "${WECHAT_MULTIPLE_INSTANCE}" in
    '')
        # Single
        :
        ;;
    'auto')
        # Multiple: tmpfs throwaway
        echo "Multiple instance: using throwaway tmpfs data dir"
        BWRAP_ARGS+=(
            --tmpfs "${HOME}/.xwechat"
            --tmpfs "${WECHAT_FILES_DIR}/all_users"
            --tmpfs "${WECHAT_FILES_DIR}/WMPF"
        )
        ;;
    *)
        # Multiple: persistent
        local INSTANCE_DATA_DIR=$(echo -n "${WECHAT_MULTIPLE_INSTANCE}" | md5sum)
        INSTANCE_DATA_DIR="${INSTANCE_DATA_DIR::32}"
        echo "Multiple instance: name '${WECHAT_MULTIPLE_INSTANCE}' => id '${INSTANCE_DATA_DIR}'"
        INSTANCE_DATA_DIR="${WECHAT_HOME_DIR}/.multi_xwechat_instance/${INSTANCE_DATA_DIR}"
        echo "Multiple instance: data dir is at '${INSTANCE_DATA_DIR}'"
        mkdir -p "${INSTANCE_DATA_DIR}"/{.xwechat,xwechat_files/all_users/config,xwechat_files/WMPF}
        BWRAP_ARGS+=(
            --bind "${INSTANCE_DATA_DIR}/.xwechat" "${HOME}/.xwechat"
            --bind "${INSTANCE_DATA_DIR}/xwechat_files/all_users" "${WECHAT_FILES_DIR}/all_users"
            --bind "${INSTANCE_DATA_DIR}/xwechat_files/WMPF" "${WECHAT_FILES_DIR}/WMPF"
        )
        ;;
    esac

    exec bwrap "${BWRAP_ARGS[@]}" "${BWRAP_CUSTOM_BINDS[@]}" "${BWRAP_DEV_BINDS[@]}" INSTALL_ROOTDIR/wechat "$@"
    echo "Error: Failed to exec bwrap, rerun this script with 'bash -x $0' to show the full command history"
    return 1
}

applet_start() {
    IFS=':' read -r -a WECHAT_CUSTOM_BINDS_RUNTIME <<< "${WECHAT_CUSTOM_BINDS}"
    if [[ -z "${WECHAT_CUSTOM_BINDS_CONFIG}" && ! -v WECHAT_CUSTOM_BINDS_CONFIG ]]; then
        WECHAT_CUSTOM_BINDS_CONFIG=~/.config/wechat-universal/binds.list
    fi
    # Parsing arguments, for any option, argument > environment
    while (( $# )); do
        case "$1" in
        '--data')
            WECHAT_DATA_DIR="$2"
            shift
            ;;
        '--bind')
            WECHAT_CUSTOM_BINDS_RUNTIME+=("$2")
            shift
            ;;
        '--binds-config')
            WECHAT_CUSTOM_BINDS_CONFIG="$2"
            shift
            ;;
        '--ime')
            WECHAT_IME_WORKAROUND="$2"
            shift
            ;;
        '--no-callout')
            WECHAT_NO_CALLOUT='yes'
            ;;
        '--multiple')
            WECHAT_NO_CALLOUT='yes'
        case "$2" in
            ''|'auto')
                WECHAT_MULTIPLE_INSTANCE='auto'
        shift
        ;;
        --*)
        WECHAT_MULTIPLE_INSTANCE='auto'
                ;;
            *)
                WECHAT_MULTIPLE_INSTANCE="$2"
                shift
                ;;
            esac
            ;;
        '--help')
            if [[ "${LANG}" == zh_CN* ]]; then
                echo "$0 (--data [微信数据文件夹]) (--bind [自定义绑定挂载] (--bind ...))) (--ime [输入法]) (--help)"
                echo
                printf '    --%s\t%s\n' \
                    'data [微信数据文件夹]' '微信数据文件夹的路径,绝对路径,或相对于用户HOME的相对路径。 默认:~/文档/Wechat_Data;环境变量: WECHAT_DATA_DIR' \
                    'bind [自定义绑定挂载]' '自定义的绑定挂载,可被声明多次,绝对路径,或相对于用户HOME的相对路径。环境变量: WECHAT_CUSTOM_BINDS, (用冒号:分隔,与PATH相似)' \
                    'binds-config [文件]' '以每行一个的方式列明应被绑定挂载的路径的纯文本配置文件,每行定义与--bind一致。默认:~/.config/wechat-universal/binds.list;环境变量:WECHAT_CUSTOM_BINDS_CONFIG' \
                    'ime [输入法名称或特殊值]' '应用输入法对应环境变量修改,可支持:fcitx (不论是否为5), ibus,特殊值:none不应用,auto自动判断。默认: auto;环境变量: WECHAT_IME_WORKAROUND'\
                    'no-callout        ' '不要试图呼出已经在运行的微信实例。默认: 不设置;环境变量: WECHAT_NO_CALLOUT'\
                    'multiple [name]   ' '创建全新的微信实例,命名为[name],即使微信已在运行,从而进行多开;强制启用--no-callout。特殊值:留空(仅参数)或auto生成一次性tmpfs数据文件夹;环境变量: WECHAT_MULTIPLE_INSTANCE'\
                    'help' ''
                echo
                echo "命令行参数比环境变量优先级更高,如果命令行参数与环境变量皆为空,则使用默认值"
            else
                echo "$0 (--data [wechat data]) (--bind [custom bind] (--bind ...))) (--ime [ime]) (--help)"
                echo
                printf '    --%s\t%s\n' \
                    'data [wechat data]' 'Path to Wechat_Data folder, absolute or relative to user home, default: ~/Documents/Wechat_Data, as environment: WECHAT_DATA_DIR' \
                    'bind [custom bind]' 'Custom bindings, could be specified multiple times, absolute or relative to user home, as environment: WECHAT_CUSTOM_BINDS (colon ":" seperated like PATH)' \
                    'binds-config [file]' 'Path to text file that contains one --bind value per line, default: ~/.config/wechat-universal/binds.list, as environment: WECHAT_CUSTOM_BINDS_CONFIG'\
                    'ime [input method]' 'Apply IME-specific workaround, support: fcitx (also for 5), ibus, default: auto, as environment: WECHAT_IME_WORKAROUND'\
                    'no-callout        ' 'do not try to call out an already running WeChat instance. default: not set, as environment: WECHAT_NO_CALLOUT'\
                    'multiple [name]' 'Create a new, individual instance even if WeChat is already running, naming it as [name], useful when you want to keep multiple WeChat accounts online on a single host, enables --no-callout implicitly. special: (leave empty as argument or) auto, to generate the data dir in a throwaway tmpfs. default: not set, as environment: WECHAT_MULTIPLE_INSTANCE'\
                    'help' ''
                echo
                echo "Arguments take priority over environment, if both argument and environment are empty, the default value would be used"
            fi
            return 0
            ;;
        *)
            echo "Unknown option: $1, pass --help to read help message"
            return 1
            ;;
        esac
        shift
    done
    if [[ -z "${WECHAT_NO_CALLOUT}" ]] && try_move_foreground; then
        return 0
    else
        try_start "$@"
        return $?
    fi
}

applet_stop() {
    notifier_item=$(notifier_get)
    [[ -z "${notifier_item}" ]] && return
    echo 'Stopping running WeChat instance'
    # Based on: https://github.com/web1n/wechat-universal-flatpak/blob/91f5d0d246881077812881e800a22f5a02cea438/wechat.sh#L35
    dbus_call "${notifier_item}" \
        /MenuBar \
        com.canonical.dbusmenu.Event \
        int32:1 \
        string:clicked \
        variant:string:'Quit WeChat' \
        uint32:0
}

applet_dde() {
    local item=
    while (( $# )); do
        case "$1" in 
        '--show-item')
            item="$2"
            shift
            ;;
        esac
        shift
    done
    if [[ "${item}" ]]; then
        local path object target
        path=$(readlink -f -- "${item}") # Resolve this to absolute path that's same across host / guest
        echo "Fake deepin file manager: dbus-send to open '${path}' in file manager"
        if [[ -d "${path}" ]]; then
            # WeChat pass both files and folders in the same way, if we use ShowItems for folders,
            # it would open that folder's parent folder, which is not right.
            object=ShowFolders
            target=folders
        else
            object=ShowItems
            target=items
        fi
        exec dbus-send --print-reply --dest=org.freedesktop.FileManager1 \
            /org/freedesktop/FileManager1 \
            org.freedesktop.FileManager1."${object}" \
            array:string:"file://${path}" \
            string:fake-dde-file-manager-show-"${target}"
        # We should not fall to here, but add a fallback anyway
        echo "Fake deepin file manager: fallback: xdg-open to show '${path}' in file manager"
        exec xdg-open "${path}"
    else
        echo "Fake deepin file manager: xdg-open with args $@"
        exec xdg-open "$@"
    fi
    # At this stage, it's either: dbus-send not found, or xdg-open not found, this should not happen
    # In whatever case, bail out
    echo "Fake deepin file manager: could not open any thing, original args: $@"
    return 1
}

applet="${0##*/}"
case "${applet}" in
    'stop.sh'|'stop')
        applet_stop "$@"
        ;;
    'start.sh'|'start'|'wechat-universal.sh'|'wechat-universal')
        applet_start "$@"
        ;;
    'dde-file-manager')
        applet_dde "$@"
        ;;
    *)
        echo "Unknown applet '${applet}', allowed: start.sh (alias start, wechat-universal, wechat-universal.sh), stop.sh (alias stop)"
        false
        ;;
esac
exit $?
openSUSE Build Service is sponsored by