mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-07-17 08:59:25 +00:00
Compare commits
184 Commits
c123e93704
...
emoji-majo
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c66005003 | |||
| f87f1bfc4b | |||
|
|
2db3076c92 | ||
| 90daed18df | |||
|
|
65dd2c645a | ||
| 89d70a940c | |||
|
|
4944517a0e | ||
| 2a7ee7f0b2 | |||
|
|
6edbd47c9a | ||
| d755c39e4a | |||
|
|
b7f8327209 | ||
| 27c82d8411 | |||
| e0b159c069 | |||
|
|
cfb19820fa | ||
|
|
e05ca13608 | ||
|
|
15cf96c01f | ||
| 2caec91a0f | |||
| 9465a1e0a8 | |||
| 251272c708 | |||
| f068a0e0fa | |||
| d0034cd112 | |||
| ae6d5ebeef | |||
|
|
8466e70424 | ||
| c3d8f27f48 | |||
| b85229f1a1 | |||
|
|
d03298a048 | ||
|
|
798e0b487e | ||
| 49444d72b4 | |||
|
|
c0e0fe4dcb | ||
|
|
9a189848ea | ||
| 96b09bfc1d | |||
| 9a6c913d89 | |||
| 42010594be | |||
| 5469c30b75 | |||
| fe9a2ee202 | |||
|
|
5e27dc7e9f | ||
|
|
69d083e3c0 | ||
| 2b95f9724e | |||
| 4066a07892 | |||
|
|
443ab58f65 | ||
| e0116acbe4 | |||
|
|
91e9bf0207 | ||
| aff0335f7e | |||
|
|
4d8baaa19b | ||
| 65452d80f1 | |||
|
|
df4d432151 | ||
|
|
1390f2c783 | ||
| be99d0e900 | |||
| 97fcf1d5fc | |||
| c9b14d7c45 | |||
| 3d0dda0347 | |||
|
|
8d69b35d67 | ||
| 7ba142a9f2 | |||
|
|
56ac55806d | ||
| 47f53c14db | |||
| a47d40b914 | |||
| be33ae30bb | |||
| 6d5f1306bf | |||
| ad3dc716e7 | |||
|
|
089b125a89 | ||
| dfd4554a11 | |||
| 1b98297881 | |||
|
|
cacc8fcafe | ||
| ac07d39db8 | |||
|
|
e27aa77cec | ||
| cbc9f68dd6 | |||
| 944d0f5a70 | |||
| 9ded7ef830 | |||
| adb18b7ce3 | |||
|
|
8220e60257 | ||
| b09af655e7 | |||
| 5833f8a3d8 | |||
|
|
72132d5e88 | ||
|
|
4167c4db78 | ||
| 3e1f6b9137 | |||
|
|
bb08b19c0d | ||
|
|
7497919e8f | ||
|
|
41cbe7b31c | ||
| 425e951341 | |||
| 2e4a0b92fc | |||
| 9f839579e2 | |||
| 900066b9d4 | |||
| 97534b0670 | |||
|
|
62672ade04 | ||
| b22e1c1ccc | |||
|
|
1b8e60c26d | ||
| 035dfdd332 | |||
| d1707d3c72 | |||
|
|
c9bed097ba | ||
| 5acb90025f | |||
|
|
2d12da79b0 | ||
|
|
bede379021 | ||
| 38acde1b20 | |||
| d0c4a5addd | |||
|
|
b8a7ccf005 | ||
| 68fc53585e | |||
| 26afc125c7 | |||
|
|
090b39f78b | ||
| 3722de6b13 | |||
|
|
d93c9aa780 | ||
| ea56504aee | |||
| 46b91edfdf | |||
|
|
d2dfde519d | ||
| 1eac56f22c | |||
|
|
fb491a6bb2 | ||
| 3da7c89b66 | |||
|
|
e842cf3a57 | ||
| 85b0e8f1a6 | |||
|
|
b8e03ff6c1 | ||
| 54b1703d67 | |||
| 15d647a0ec | |||
|
|
353558ddb3 | ||
|
|
9f58d30050 | ||
|
|
4f0632cdf1 | ||
|
|
ff8ba71948 | ||
|
|
a367591327 | ||
|
|
c91925fbc2 | ||
| 479f1649ef | |||
|
|
06f69184b6 | ||
|
|
3471d9a57c | ||
| 28d4035477 | |||
|
|
3e1d151efd | ||
| 556cfc3036 | |||
| c49b757be6 | |||
| cec7e2caf3 | |||
| 9a0c07a461 | |||
| c7064c18b9 | |||
|
|
fb897310c8 | ||
| 5944849326 | |||
| 1255f1e6c7 | |||
| d7bc09d859 | |||
| f84a448351 | |||
| 7e34690e0c | |||
|
|
6f6d378600 | ||
|
|
216b463b8f | ||
| 4ad84cb5e5 | |||
| 19e0946717 | |||
| 0de1ad06cd | |||
|
|
72427006dd | ||
|
|
0e5f702651 | ||
| 4f1b0fc838 | |||
|
|
23d0963c2d | ||
|
|
dcb35fb80f | ||
| b1768854c3 | |||
| 4ed4eaf8e8 | |||
|
|
d167b53b17 | ||
| 18eff80a30 | |||
| a44ec5f624 | |||
|
|
5da518f50e | ||
|
|
ebb11a8ac5 | ||
|
|
dd9c5db1aa | ||
| 984ac9763e | |||
|
|
ad67af2f8f | ||
| cc8460d366 | |||
| 1dc9b9c956 | |||
|
|
20b552ad9d | ||
|
|
12f4885ad7 | ||
| 9c1fdef286 | |||
| c125a154da | |||
| 028d768b23 | |||
| 42972e01b0 | |||
| 1bd37af7d0 | |||
| 5fad2d0c02 | |||
|
|
bd9adeb795 | ||
|
|
5fd3c7c9d9 | ||
| 937184429f | |||
| e31ccff3f8 | |||
| edaafba7eb | |||
| 87018fa7fe | |||
| 8fcec519d0 | |||
| aa52236c01 | |||
| cd5b61d9d0 | |||
| 4eefdd6011 | |||
|
|
21748b3133 | ||
|
|
e7db202f88 | ||
| a93e76eaf4 | |||
| 3c5bdb2ab2 | |||
| 8f7c40caf1 | |||
| 150ad62771 | |||
|
|
5e3715dc31 | ||
| 05648ba8e3 | |||
|
|
0956471a10 | ||
| 1f79e62f53 | |||
| e49333340f |
132
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
132
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a problem with ZoiteChat
|
||||
title: "[Bug]: "
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
```
|
||||
+------------------------------------------------------------+
|
||||
| ZoiteChat Bug Report |
|
||||
| Please fill this out so the bug goblin has enough crumbs. |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
What went wrong?
|
||||
|
||||
```text
|
||||
Describe the bug clearly and briefly.
|
||||
```
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
```text
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
```
|
||||
|
||||
## Expected Result
|
||||
|
||||
```text
|
||||
What should have happened?
|
||||
```
|
||||
|
||||
## Actual Result
|
||||
|
||||
```text
|
||||
What happened instead?
|
||||
```
|
||||
|
||||
## ZoiteChat Version
|
||||
|
||||
```text
|
||||
ZoiteChat version:
|
||||
Build/source: GitHub release / Fedora package / source build / Windows installer / other
|
||||
Commit hash, if built from source:
|
||||
```
|
||||
|
||||
## System Information
|
||||
|
||||
```text
|
||||
OS:
|
||||
OS version:
|
||||
Desktop environment:
|
||||
Display server: Wayland / X11 / Windows / other
|
||||
CPU architecture: x86_64 / aarch64 / other
|
||||
```
|
||||
|
||||
## IRC / Network Details
|
||||
|
||||
```text
|
||||
IRC network/server:
|
||||
Port:
|
||||
TLS/SSL enabled: yes / no
|
||||
SASL enabled: yes / no
|
||||
Using bouncer/ZNC: yes / no
|
||||
Proxy/VPN involved: yes / no
|
||||
IRCv3 capabilities involved, if known:
|
||||
```
|
||||
|
||||
## Plugins, Scripts, and Config
|
||||
|
||||
```text
|
||||
Plugins enabled:
|
||||
Scripts loaded:
|
||||
Theme/custom CSS:
|
||||
Custom config changes:
|
||||
Does the bug still happen with a clean config? yes / no / untested
|
||||
```
|
||||
|
||||
## Logs / Terminal Output
|
||||
|
||||
Paste relevant output below.
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
## Raw IRC Lines, If Relevant
|
||||
|
||||
Remove passwords, tokens, IPs, private channels, private messages, certs, and account details before posting.
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
## Crash Details, If Relevant
|
||||
|
||||
```text
|
||||
Did ZoiteChat crash? yes / no
|
||||
Crash reporter output:
|
||||
Backtrace:
|
||||
Core dump available: yes / no
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
Attach screenshots or screen recordings if the issue is visual.
|
||||
|
||||
## Regression Check
|
||||
|
||||
```text
|
||||
Did this work in an older version? yes / no / unknown
|
||||
Last known working version:
|
||||
First broken version:
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I searched existing issues first.
|
||||
* [ ] I tested with the latest ZoiteChat version available to me.
|
||||
* [ ] I included steps to reproduce the issue.
|
||||
* [ ] I removed private information from logs and raw IRC lines.
|
||||
* [ ] I included system and network details where relevant.
|
||||
|
||||
## Extra Notes
|
||||
|
||||
```text
|
||||
Anything else that might help.
|
||||
```
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
57
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
57
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an improvement for ZoiteChat
|
||||
title: "[Feature]: "
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
```text
|
||||
Describe the feature or enhancement clearly and briefly.
|
||||
```
|
||||
|
||||
## Problem
|
||||
|
||||
```text
|
||||
What problem would this solve?
|
||||
```
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
```text
|
||||
How should ZoiteChat behave?
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
```text
|
||||
Other approaches or workarounds you considered.
|
||||
```
|
||||
|
||||
## Use Case
|
||||
|
||||
```text
|
||||
Who benefits from this, and when would they use it?
|
||||
```
|
||||
|
||||
## Platform Details
|
||||
|
||||
```text
|
||||
OS:
|
||||
ZoiteChat version:
|
||||
Build/source: GitHub release / Fedora package / source build / Windows installer / other
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I searched existing issues first.
|
||||
* [ ] I checked whether this already exists in the latest ZoiteChat version available to me.
|
||||
* [ ] I explained the problem this would solve.
|
||||
|
||||
## Extra Notes
|
||||
|
||||
```text
|
||||
Anything else that might help.
|
||||
```
|
||||
64
.github/workflows/appimage-build.yml
vendored
64
.github/workflows/appimage-build.yml
vendored
@@ -28,15 +28,15 @@ jobs:
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential pkg-config meson ninja-build cmake \
|
||||
gettext \
|
||||
libcanberra-dev libdbus-glib-1-dev libglib2.0-dev \
|
||||
libcanberra-dev libglib2.0-dev \
|
||||
libarchive-dev \
|
||||
libgtk-3-dev \
|
||||
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
|
||||
libxkbcommon0 \
|
||||
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
|
||||
libluajit-5.1-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
|
||||
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
|
||||
perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
|
||||
fonts-noto-color-emoji breeze-gtk-theme \
|
||||
breeze-gtk-theme \
|
||||
patchelf file curl
|
||||
|
||||
- name: Configure
|
||||
@@ -86,11 +86,21 @@ jobs:
|
||||
cp -a /usr/lib/x86_64-linux-gnu/python3/dist-packages AppDir/usr/lib/x86_64-linux-gnu/python3/
|
||||
fi
|
||||
|
||||
if [ -d "/usr/lib/x86_64-linux-gnu/perl-base" ]; then
|
||||
install -d AppDir/usr/lib/x86_64-linux-gnu
|
||||
cp -a /usr/lib/x86_64-linux-gnu/perl-base AppDir/usr/lib/x86_64-linux-gnu/
|
||||
fi
|
||||
|
||||
if [ -d "/usr/lib/x86_64-linux-gnu/perl" ]; then
|
||||
install -d AppDir/usr/lib/x86_64-linux-gnu
|
||||
cp -a /usr/lib/x86_64-linux-gnu/perl AppDir/usr/lib/x86_64-linux-gnu/
|
||||
fi
|
||||
|
||||
if [ -d "/usr/lib/x86_64-linux-gnu/perl5" ]; then
|
||||
install -d AppDir/usr/lib/x86_64-linux-gnu
|
||||
cp -a /usr/lib/x86_64-linux-gnu/perl5 AppDir/usr/lib/x86_64-linux-gnu/
|
||||
fi
|
||||
|
||||
if [ -d "/usr/share/perl" ]; then
|
||||
install -d AppDir/usr/share
|
||||
cp -a /usr/share/perl AppDir/usr/share/
|
||||
@@ -100,20 +110,18 @@ jobs:
|
||||
install -d AppDir/usr/share
|
||||
cp -a /usr/share/perl5 AppDir/usr/share/
|
||||
fi
|
||||
|
||||
perl -MFile::Spec -e 'print "Build host File::Spec: $INC{\"File/Spec.pm\"}\n"'
|
||||
find AppDir/usr -path '*/File/Spec.pm' -print -quit | grep -q .
|
||||
|
||||
if compgen -G '/usr/lib/x86_64-linux-gnu/libpython3*.so*' > /dev/null; then
|
||||
install -d AppDir/usr/lib/x86_64-linux-gnu
|
||||
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
|
||||
fi
|
||||
|
||||
if [ -d "/usr/share/gtk-3.0/emoji" ]; then
|
||||
install -d AppDir/usr/share/gtk-3.0
|
||||
cp -a /usr/share/gtk-3.0/emoji AppDir/usr/share/gtk-3.0/
|
||||
fi
|
||||
|
||||
if [ -f "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf" ]; then
|
||||
install -d AppDir/usr/share/fonts/truetype/noto
|
||||
cp -a /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf AppDir/usr/share/fonts/truetype/noto/
|
||||
fi
|
||||
# The emoji font is not bundled on Linux; the AppImage uses the
|
||||
# host system's emoji font (e.g. fonts-noto-color-emoji).
|
||||
test ! -e AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf
|
||||
|
||||
if [ -d "/usr/lib/x86_64-linux-gnu/gtk-3.0/modules" ]; then
|
||||
install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0
|
||||
@@ -129,7 +137,6 @@ jobs:
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
# Include every built-in plugin except the Windows-only update plugin.
|
||||
find AppDir/usr/lib -maxdepth 4 -type f -name '*.so' -path '*/zoitechat/plugins/*' -print | sort
|
||||
|
||||
for plugin in checksum fishlim lua perl python sysinfo; do
|
||||
@@ -162,7 +169,7 @@ jobs:
|
||||
|
||||
APPDIR="${APPDIR:-$(dirname "$(readlink -f "$0")")}"
|
||||
|
||||
export PATH="${PATH:-/usr/bin:/bin}:$APPDIR/usr/bin"
|
||||
export PATH="$APPDIR/usr/bin:${PATH:-/usr/bin:/bin}"
|
||||
export LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH:-}"
|
||||
export XDG_DATA_DIRS="$APPDIR/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
export GTK_EXE_PREFIX="$APPDIR/usr"
|
||||
@@ -178,14 +185,6 @@ jobs:
|
||||
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
|
||||
fi
|
||||
|
||||
if [ -d "$APPDIR/etc/fonts" ]; then
|
||||
export FONTCONFIG_SYSROOT="$APPDIR"
|
||||
export FONTCONFIG_PATH="$APPDIR/etc/fonts${FONTCONFIG_PATH:+:$FONTCONFIG_PATH}"
|
||||
if [ -f "$APPDIR/etc/fonts/fonts.conf" ]; then
|
||||
export FONTCONFIG_FILE="$APPDIR/etc/fonts/fonts.conf"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then
|
||||
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
|
||||
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then
|
||||
@@ -211,6 +210,23 @@ jobs:
|
||||
|
||||
unset GTK_MODULES
|
||||
|
||||
perl5lib_entries=""
|
||||
for dir in \
|
||||
"$APPDIR/usr/lib/x86_64-linux-gnu/perl-base" \
|
||||
"$APPDIR/usr/lib/x86_64-linux-gnu/perl"/* \
|
||||
"$APPDIR/usr/lib/x86_64-linux-gnu/perl5"/* \
|
||||
"$APPDIR/usr/share/perl"/* \
|
||||
"$APPDIR/usr/share/perl5"
|
||||
do
|
||||
if [ -d "$dir" ]; then
|
||||
perl5lib_entries="${perl5lib_entries:+$perl5lib_entries:}$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$perl5lib_entries" ]; then
|
||||
export PERL5LIB="$perl5lib_entries${PERL5LIB:+:$PERL5LIB}"
|
||||
fi
|
||||
|
||||
export PYTHONHOME="$APPDIR/usr"
|
||||
python_stdlib_dir="$(find "$APPDIR/usr/lib" -maxdepth 1 -type d -name 'python3.*' | head -n 1 || true)"
|
||||
pythonpath_entries=""
|
||||
@@ -227,13 +243,10 @@ jobs:
|
||||
export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}"
|
||||
fi
|
||||
|
||||
# OpenSSL trust store override
|
||||
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}"
|
||||
export SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
|
||||
export CURL_CA_BUNDLE="${CURL_CA_BUNDLE:-$SSL_CERT_FILE}"
|
||||
|
||||
# Prefer Wayland if the session provides it, but keep X11 fallback.
|
||||
# Don't override if the user already set GDK_BACKEND explicitly.
|
||||
if [ -z "${GDK_BACKEND:-}" ]; then
|
||||
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
|
||||
export GDK_BACKEND="wayland,x11"
|
||||
@@ -256,7 +269,6 @@ jobs:
|
||||
EOF
|
||||
chmod +x AppRun
|
||||
|
||||
|
||||
VERSION="$(git describe --tags --always)"
|
||||
|
||||
./linuxdeploy-x86_64.AppImage \
|
||||
|
||||
3
.github/workflows/flatpak-build.yml
vendored
3
.github/workflows/flatpak-build.yml
vendored
@@ -25,6 +25,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Generate PSL list
|
||||
run: curl -fsSL https://publicsuffix.org/list/public_suffix_list.dat -o src/common/public_suffix_list.dat
|
||||
|
||||
- name: Build Flatpak
|
||||
id: flatpak_builder
|
||||
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
||||
|
||||
81
.github/workflows/manjaro-package-build.yml
vendored
81
.github/workflows/manjaro-package-build.yml
vendored
@@ -1,81 +0,0 @@
|
||||
name: Manjaro Package Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
manjaro-package:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: manjarolinux/base:latest
|
||||
|
||||
steps:
|
||||
- name: Install packaging toolchain
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
pacman -Syu --noconfirm
|
||||
pacman -S --noconfirm --needed \
|
||||
base-devel \
|
||||
git \
|
||||
namcap \
|
||||
meson \
|
||||
ninja \
|
||||
pkgconf \
|
||||
glib2 \
|
||||
glib2-devel \
|
||||
gtk3 \
|
||||
openssl \
|
||||
dbus-glib \
|
||||
libcanberra \
|
||||
libayatana-appindicator \
|
||||
luajit \
|
||||
iso-codes \
|
||||
lua \
|
||||
perl \
|
||||
python \
|
||||
python-cffi \
|
||||
pciutils
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Build package and generate .SRCINFO (non-root)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
id -u builder >/dev/null 2>&1 || useradd -m builder
|
||||
chown -R builder:builder "$GITHUB_WORKSPACE"
|
||||
su builder -c "cd '$GITHUB_WORKSPACE/packaging/manjaro' && makepkg --noconfirm --syncdeps --cleanbuild"
|
||||
su builder -c "cd '$GITHUB_WORKSPACE/packaging/manjaro' && makepkg --printsrcinfo > .SRCINFO"
|
||||
|
||||
- name: Lint PKGBUILD and package with namcap
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
cd "$GITHUB_WORKSPACE/packaging/manjaro"
|
||||
namcap PKGBUILD
|
||||
pkg_file="$(find . -maxdepth 1 -type f -name '*.pkg.tar.*' | head -n1)"
|
||||
namcap "$pkg_file"
|
||||
|
||||
- name: Collect artifacts
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
mkdir -p artifacts
|
||||
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/*.pkg.tar.* artifacts/
|
||||
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/PKGBUILD artifacts/
|
||||
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/.SRCINFO artifacts/
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: zoitechat-manjaro-package
|
||||
path: artifacts/*
|
||||
if-no-files-found: error
|
||||
45
.github/workflows/windows-build.yml
vendored
45
.github/workflows/windows-build.yml
vendored
@@ -65,8 +65,8 @@ jobs:
|
||||
Download-WithRetry -Url https://github.com/jrsoftware/issrc/releases/download/is-6_7_1/innosetup-6.7.1.exe -OutFile deps\innosetup-unicode.exe
|
||||
& deps\innosetup-unicode.exe /VERYSILENT | Out-Null
|
||||
|
||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.0-pre1/GTK3_Gvsbuild_zoitechat-2.18.0-pre1_${{ matrix.platform }}.7z -OutFile deps\gtk-${{ matrix.arch }}.7z
|
||||
& 7z.exe x deps\gtk-${{ matrix.arch }}.7z -oC:\gtk-build\gtk\x64\release
|
||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.1/GTK3_Gvsbuild_zoitechat-2.18.1_x64.zip -OutFile deps\gtk-${{ matrix.arch }}.zip
|
||||
Expand-Archive -LiteralPath deps\gtk-${{ matrix.arch }}.zip -DestinationPath C:\gtk-build\gtk\x64\release -Force
|
||||
|
||||
Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst
|
||||
python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\hicolor-icon-theme.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith('mingw64/share/icons/hicolor/')];tf.close();reader.close();f.close()"
|
||||
@@ -113,6 +113,26 @@ jobs:
|
||||
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
|
||||
}
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.8.10'
|
||||
architecture: ${{ matrix.arch }}
|
||||
|
||||
- name: Prepare Python 3.8
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install cffi
|
||||
|
||||
$pyRoot = $env:pythonLocation
|
||||
if (-not $pyRoot) { $pyRoot = & python -c "import sys; print(sys.prefix)" }
|
||||
|
||||
foreach ($pyDir in @("C:\gtk-build\python-3.8.10", "C:\gtk-build\python-3.8")) {
|
||||
New-Item -Path $pyDir -ItemType Directory -Force | Out-Null
|
||||
$target = Join-Path $pyDir "${{ matrix.platform }}"
|
||||
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
|
||||
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
|
||||
}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
|
||||
@@ -124,12 +144,33 @@ jobs:
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "PYTHON38_DIR=C:\gtk-build\python-3.8\${{ matrix.platform }}"
|
||||
if not exist "%PYTHON38_DIR%\libs\python38.lib" (
|
||||
echo Missing %PYTHON38_DIR%\libs\python38.lib
|
||||
dir "%PYTHON38_DIR%\libs"
|
||||
exit /b 1
|
||||
)
|
||||
if not exist "%PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd" (
|
||||
echo Missing %PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "LIB=%PYTHON_DIR%\libs;%LIB%"
|
||||
set "INCLUDE=%PYTHON_DIR%\include;%INCLUDE%"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$archiveLib='C:\gtk-build\gtk\x64\release\lib\libarchive.lib'; if (-not (Test-Path $archiveLib)) { $archiveDll = Get-ChildItem 'C:\gtk-build\gtk\x64\release\bin\libarchive*.dll' | Select-Object -First 1; if ($archiveDll) { Push-Location 'C:\gtk-build\gtk\x64\release\lib'; & 'C:\gtk-build\gendef\gendef.exe' $archiveDll.FullName | Out-Null; $archiveDef = Get-ChildItem 'libarchive*.def' | Select-Object -First 1; if ($archiveDef) { & lib /def:$archiveDef.Name /machine:${{ matrix.platform }} /out:libarchive.lib | Out-Null }; Pop-Location } }"
|
||||
|
||||
msbuild win32\zoitechat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\hcpython38.dll" (
|
||||
echo hcpython38.dll was not built
|
||||
exit /b 1
|
||||
)
|
||||
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\_cffi_backend.cp38*.pyd" (
|
||||
echo _cffi_backend for Python 3.8 was not staged for the installer
|
||||
exit /b 1
|
||||
)
|
||||
shell: cmd
|
||||
|
||||
- name: Preparing Artifacts
|
||||
|
||||
69
COPYING
69
COPYING
@@ -1,12 +1,12 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
@@ -15,7 +15,7 @@ software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
@@ -55,8 +55,8 @@ patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions:
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
@@ -225,7 +225,7 @@ impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
@@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
@@ -277,9 +277,9 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
@@ -291,7 +291,7 @@ convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) 19yy <name of author>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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
|
||||
@@ -303,17 +303,15 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) 19yy name of author
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
@@ -330,11 +328,34 @@ necessary. Here is a sample; alter the names:
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
<signature of Moe Ghoul>, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
|
||||
|
||||
======================================================================
|
||||
ADDITIONAL LICENSE NOTICE
|
||||
======================================================================
|
||||
|
||||
ZoiteChat source code:
|
||||
GPL version 2
|
||||
See Above.
|
||||
|
||||
Unicode Emoji and CLDR data:
|
||||
Unicode License v3
|
||||
data/emoji/LICENSE-UNICODE.txt
|
||||
|
||||
gemoji shortcode data:
|
||||
MIT License
|
||||
Copyright (c) 2019 GitHub, Inc.
|
||||
data/emoji/LICENSE-GEMOJI.txt
|
||||
|
||||
Noto Color Emoji:
|
||||
SIL Open Font License 1.1
|
||||
data/fonts/OFL.txt
|
||||
|
||||
@@ -1,7 +1,75 @@
|
||||
ZoiteChat ChangeLog
|
||||
=================
|
||||
|
||||
2.18.3 (2026-06-29)
|
||||
-------------------
|
||||
|
||||
- Added IRCv3 support for message tags, echo-message, typing notifications, and replies.
|
||||
- Restored the accept-invalid-cert TLS bypass behavior.
|
||||
- Replaced legacy IPv4 APIs and added support for OpenSSL 4 APIs.
|
||||
- Snapshot Python hooks during plugin unload for safer plugin shutdown.
|
||||
- Fixed Windows GtkStatusIcon tray menu popup behavior.
|
||||
- Fixed self-echoed private messages routing to the target tab.
|
||||
- Fixed the lag ping timeout window.
|
||||
|
||||
2.18.2 (2026-06-20)
|
||||
-------------------
|
||||
|
||||
- Improved security and connection handling with stronger TLS defaults,
|
||||
safer certificate/hostname validation, FiSHLiM OpenSSL fixes, keyring support,
|
||||
and getaddrinfo-based DCC lookups.
|
||||
- Expanded FiSHLiM usability with a GTK key manager, context-menu access, and clearer
|
||||
channel/private-message key handling.
|
||||
- Improved keyboard and navigation behavior with customizable keybinds, reset support,
|
||||
channel-name switching, /server -noproxy, and configurable stale-link ping checks.
|
||||
- Refined the GTK interface with native file chooser dialogs, topic layout preferences,
|
||||
timestamp/date hover tooltips, better restore-down relayout, text scroll speed controls,
|
||||
and aligned network meters.
|
||||
- Added privacy and display options, including hiding join/part hostmasks and improved
|
||||
topic/multiline mode layout controls.
|
||||
- Fixed several platform/UI edge cases, including tray restore from iconified state, DBus unload
|
||||
handling, GTK3 theme refresh/saving, and tab switcher compatibility.
|
||||
|
||||
2.18.1 (2026-05-21)
|
||||
-------------------
|
||||
|
||||
- Migrated D-Bus handling to GDBus.
|
||||
- Enabled mouse wheel channel switching by default and consumed handled tab wheel events.
|
||||
- Switched main panes to native GTK scrollbars.
|
||||
- Tightened userlist button and meter layout.
|
||||
- Made the native Windows file chooser modal.
|
||||
- Set the Windows AppUserModelID before GTK startup.
|
||||
- Updated Windows CI to use the new GTK3 bundle zip.
|
||||
- Updated Windows installer architecture checks to use x64-compatible detection.
|
||||
- Bumped the Flatpak Perl runtime to 5.42.2.
|
||||
- Fixed palette color reads to preserve base GTK state.
|
||||
- Fixed auto-replace whole-word matching.
|
||||
- Fixed checksum file stream cleanup.
|
||||
- Fixed byte handling in the Python console.
|
||||
- Guarded GTK drag-and-drop handlers against null windows.
|
||||
- Fixed size_t and integer handling issues.
|
||||
- Removed the Winamp plugin.
|
||||
|
||||
2.18.0 (2026-04-20)
|
||||
-------------------
|
||||
|
||||
- Added optional close buttons on tabs.
|
||||
- Added Ctrl+W to close tabs and Ctrl+Shift+T to reopen recently closed tabs.
|
||||
- Expanded theme import support with .hct support in colors.conf import.
|
||||
- Added pevent import support from .hct theme files and improved import result messaging.
|
||||
- Expanded palette and selection CSS styling for better theme consistency.
|
||||
- Auto-sizes the topic bar to wrapped text for better multi-line topic handling.
|
||||
- Refined command-character parsing when pasting path-like text.
|
||||
- Tightened topic link detection and opening rules.
|
||||
- Added Public Suffix List validation for host links to reduce bad link matches.
|
||||
- Improved channel tree behavior by ignoring collapsed parent selections.
|
||||
- Sanitized the Linux open environment for AppImage builds and added safer fallback handling.
|
||||
- Labeled Windows installer runtimes with exact versions.
|
||||
- Fixed the notification icon and corrected the Flatpak screenshot asset.
|
||||
- Improved AppStream metainfo validation.
|
||||
|
||||
2.18.0~pre6 (2026-03-30)
|
||||
------------------------
|
||||
|
||||
- Applied app theme CSS to the menubar consistently across the app.
|
||||
- Restored horizontal separator lines in menus.
|
||||
|
||||
39
data/emoji/LICENSE-UNICODE.txt
Normal file
39
data/emoji/LICENSE-UNICODE.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
UNICODE LICENSE V3
|
||||
|
||||
COPYRIGHT AND PERMISSION NOTICE
|
||||
|
||||
Copyright © 1991-2026 Unicode, Inc.
|
||||
|
||||
NOTICE TO USER: Carefully read the following legal agreement. BY
|
||||
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
||||
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
||||
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
||||
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of data files and any associated documentation (the "Data Files") or
|
||||
software and any associated documentation (the "Software") to deal in the
|
||||
Data Files or Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
||||
copies of the Data Files or Software, and to permit persons to whom the
|
||||
Data Files or Software are furnished to do so, provided that either (a)
|
||||
this copyright and permission notice appear with all copies of the Data
|
||||
Files or Software, or (b) this copyright and permission notice appear in
|
||||
associated Documentation.
|
||||
|
||||
THE DATA FILES AND SOFTWARE ARE 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 OF
|
||||
THIRD PARTY RIGHTS.
|
||||
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
||||
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
||||
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
||||
FILES OR SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall
|
||||
not be used in advertising or otherwise to promote the sale, use or other
|
||||
dealings in these Data Files or Software without prior written
|
||||
authorization of the copyright holder.
|
||||
30
data/emoji/README.md
Normal file
30
data/emoji/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Bundled emoji data
|
||||
|
||||
These are pinned copies of the upstream data files that
|
||||
`tools/gen-emoji-data.py` compiles into ZoiteChat's emoji picker catalog
|
||||
at build time. Bundling them means every ZoiteChat package exposes the
|
||||
same emoji, names, categories and search keywords, independent of the
|
||||
GTK runtime version or the emoji data a distribution installs under
|
||||
`/usr/share/gtk-3.0/emoji`.
|
||||
|
||||
Nothing here affects what is sent over IRC: the picker inserts plain
|
||||
Unicode sequences. Official builds register the bundled Noto Color Emoji
|
||||
font privately for consistent rendering, while preserving platform font
|
||||
fallback if registration is unavailable.
|
||||
|
||||
| File | Source | Version |
|
||||
| ---- | ------ | ------- |
|
||||
| `emoji-test.txt` | [Unicode emoji data](https://unicode.org/Public/emoji/17.0/emoji-test.txt) (mirrored at [unicode-org/unicodetools](https://github.com/unicode-org/unicodetools/blob/main/unicodetools/data/emoji/17.0/emoji-test.txt)) | Emoji 17.0 |
|
||||
| `cldr-annotations-en.xml` | [unicode-org/cldr](https://github.com/unicode-org/cldr/blob/release-48/common/annotations/en.xml) `common/annotations/en.xml` | CLDR release 48 |
|
||||
| `cldr-annotations-derived-en.xml` | [unicode-org/cldr](https://github.com/unicode-org/cldr/blob/release-48/common/annotationsDerived/en.xml) `common/annotationsDerived/en.xml` | CLDR release 48 |
|
||||
| `gemoji.json` | [github/gemoji](https://github.com/github/gemoji/blob/v4.1.0/db/emoji.json) `db/emoji.json` | v4.1.0 |
|
||||
|
||||
The pinned versions are recorded in `VERSIONS`.
|
||||
|
||||
## Licenses
|
||||
|
||||
- `emoji-test.txt`, `cldr-annotations-en.xml`,
|
||||
`cldr-annotations-derived-en.xml`: © Unicode, Inc., distributed under
|
||||
the [Unicode License v3](https://www.unicode.org/license.txt).
|
||||
- `gemoji.json`: from the gemoji project, MIT licensed,
|
||||
© GitHub, Inc.
|
||||
6
data/emoji/VERSIONS
Normal file
6
data/emoji/VERSIONS
Normal file
@@ -0,0 +1,6 @@
|
||||
# Pinned versions of the emoji source data in this directory.
|
||||
# Keep in sync with the files themselves; tools/gen-emoji-data.py
|
||||
# verifies unicode-emoji against the emoji-test.txt header.
|
||||
unicode-emoji=17.0
|
||||
cldr=48
|
||||
gemoji=4.1.0
|
||||
4749
data/emoji/cldr-annotations-derived-en.xml
Normal file
4749
data/emoji/cldr-annotations-derived-en.xml
Normal file
File diff suppressed because it is too large
Load Diff
3957
data/emoji/cldr-annotations-en.xml
Normal file
3957
data/emoji/cldr-annotations-en.xml
Normal file
File diff suppressed because it is too large
Load Diff
5518
data/emoji/emoji-test.txt
Normal file
5518
data/emoji/emoji-test.txt
Normal file
File diff suppressed because it is too large
Load Diff
23479
data/emoji/gemoji.json
Normal file
23479
data/emoji/gemoji.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
data/fonts/NotoColorEmoji.ttf
Normal file
BIN
data/fonts/NotoColorEmoji.ttf
Normal file
Binary file not shown.
91
data/fonts/OFL.txt
Normal file
91
data/fonts/OFL.txt
Normal file
@@ -0,0 +1,91 @@
|
||||
Copyright 2013 Google LLC
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) shall not be
|
||||
used to promote, endorse or advertise any Modified Version, except to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s)
|
||||
or with their explicit written permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
23
data/fonts/README.md
Normal file
23
data/fonts/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Bundled emoji font
|
||||
|
||||
`NotoColorEmoji.ttf` reports Noto Color Emoji version 2.051 and covers
|
||||
Unicode Emoji 17.0. It is vendored from Debian's
|
||||
`fonts-noto-color-emoji` 2.051 package, built from the corresponding
|
||||
Google Noto Emoji release.
|
||||
|
||||
The font is bundled only with Windows builds, where it is installed
|
||||
inside the application and registered only for the ZoiteChat process.
|
||||
It is not installed as a global operating-system font.
|
||||
|
||||
Linux (and other Unix) builds do not install this file. They rely on
|
||||
the emoji font provided by the operating system instead — typically the
|
||||
distribution's Noto Color Emoji package (`fonts-noto-color-emoji` on
|
||||
Debian/Ubuntu, `google-noto-color-emoji-fonts` on Fedora).
|
||||
|
||||
Upstream release: https://github.com/googlefonts/noto-emoji/releases/tag/v2.051
|
||||
|
||||
Vendored file SHA-256:
|
||||
`dac5c27651082d6c53dab5081f50d2022ddf6877d730fa919cf6a4fc2af22de0`
|
||||
|
||||
The font is distributed under the SIL Open Font License 1.1. See
|
||||
`OFL.txt`.
|
||||
13
data/fonts/meson.build
Normal file
13
data/fonts/meson.build
Normal file
@@ -0,0 +1,13 @@
|
||||
emoji_font_dir = join_paths(get_option('datadir'), 'fonts', 'zoitechat')
|
||||
emoji_font_doc_dir = join_paths(get_option('datadir'), 'doc', 'zoitechat', 'fonts')
|
||||
|
||||
install_data(
|
||||
'NotoColorEmoji.ttf',
|
||||
install_dir: emoji_font_dir,
|
||||
)
|
||||
|
||||
install_data(
|
||||
'OFL.txt',
|
||||
'README.md',
|
||||
install_dir: emoji_font_doc_dir,
|
||||
)
|
||||
@@ -3,6 +3,11 @@ if get_option('plugin')
|
||||
endif
|
||||
|
||||
if get_option('gtk-frontend')
|
||||
# The bundled emoji font ships only with Windows builds; Linux and other
|
||||
# Unix systems use the system emoji font package instead.
|
||||
if host_machine.system() == 'windows'
|
||||
subdir('fonts')
|
||||
endif
|
||||
subdir('icons')
|
||||
subdir('misc')
|
||||
subdir('man')
|
||||
|
||||
@@ -22,7 +22,7 @@ if get_option('gtk-frontend')
|
||||
endif
|
||||
|
||||
desktop_conf = configuration_data()
|
||||
if dbus_glib_dep.found()
|
||||
if dbus_dep.found()
|
||||
desktop_conf.set('exec_command', 'zoitechat --existing %U')
|
||||
else
|
||||
desktop_conf.set('exec_command', 'zoitechat %U')
|
||||
|
||||
@@ -29,6 +29,94 @@
|
||||
<id>zoitechat.desktop</id>
|
||||
</provides>
|
||||
<releases>
|
||||
<release date="2026-06-29" version="2.18.3">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added IRCv3 support for message tags, echo-message, typing notifications, and replies.</li>
|
||||
<li>Restored the accept-invalid-cert TLS bypass behavior.</li>
|
||||
<li>Replaced legacy IPv4 APIs and added support for OpenSSL 4 APIs.</li>
|
||||
<li>Snapshot Python hooks during plugin unload for safer plugin shutdown.</li>
|
||||
<li>Fixed Windows GtkStatusIcon tray menu popup behavior.</li>
|
||||
<li>Fixed self-echoed private messages routing to the target tab.</li>
|
||||
<li>Fixed the lag ping timeout window.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-06-20" version="2.18.2">
|
||||
<description>
|
||||
<p>Security and connections:</p>
|
||||
<ul>
|
||||
<li>Improved TLS, certificate and hostname validation, FiSHLiM OpenSSL handling, keyring support, and DCC host lookups.</li>
|
||||
</ul>
|
||||
<p>FiSHLiM:</p>
|
||||
<ul>
|
||||
<li>Added a GTK key manager with context-menu access and clearer channel/private-message key handling.</li>
|
||||
</ul>
|
||||
<p>Interface and preferences:</p>
|
||||
<ul>
|
||||
<li>Improved keybind customization, reset support, channel-name switching, <code>/server -noproxy</code>, and stale-link ping checks.</li>
|
||||
<li>Refined GTK behavior with native file choosers, topic layout preferences, timestamp/date tooltips, text scroll controls, and aligned network meters.</li>
|
||||
<li>Added privacy and display options for hiding join/part hostmasks and controlling topic/multiline layout.</li>
|
||||
</ul>
|
||||
<p>Fixes:</p>
|
||||
<ul>
|
||||
<li>Fixed tray restore, DBus unload handling, GTK3 theme refresh/saving, topic relayout, and tab switcher compatibility.</li>
|
||||
<li>Updated project documentation.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-05-21" version="2.18.1">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Migrated D-Bus handling to GDBus.</li>
|
||||
<li>Enabled mouse wheel channel switching by default and consumed handled tab wheel events.</li>
|
||||
<li>Switched main panes to native GTK scrollbars.</li>
|
||||
<li>Tightened userlist button and meter layout.</li>
|
||||
<li>Made the native Windows file chooser modal.</li>
|
||||
<li>Set the Windows AppUserModelID before GTK startup.</li>
|
||||
<li>Updated Windows CI to use the new GTK3 bundle zip.</li>
|
||||
<li>Updated Windows installer architecture checks to use x64-compatible detection.</li>
|
||||
<li>Bumped the Flatpak Perl runtime to 5.42.2.</li>
|
||||
<li>Fixed palette color reads to preserve base GTK state.</li>
|
||||
<li>Fixed auto-replace whole-word matching.</li>
|
||||
<li>Fixed checksum file stream cleanup.</li>
|
||||
<li>Fixed byte handling in the Python console.</li>
|
||||
<li>Guarded GTK drag-and-drop handlers against null windows.</li>
|
||||
<li>Fixed size_t and integer handling issues.</li>
|
||||
<li>Removed the Winamp plugin.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-04-20" version="2.18.0">
|
||||
<description>
|
||||
<p>Tabs and navigation:</p>
|
||||
<ul>
|
||||
<li>Added optional close buttons on tabs.</li>
|
||||
<li>Added Ctrl+W to close tabs and Ctrl+Shift+T to reopen recently closed tabs.</li>
|
||||
<li>Improved channel tree behavior by ignoring collapsed parent selections.</li>
|
||||
</ul>
|
||||
<p>Themes and appearance:</p>
|
||||
<ul>
|
||||
<li>Expanded theme import support with .hct support in colors.conf import.</li>
|
||||
<li>Added pevent import support from .hct theme files and improved import result messaging.</li>
|
||||
<li>Expanded palette and selection CSS styling for better theme consistency.</li>
|
||||
<li>Auto-sizes the topic bar to wrapped text for better multi-line topic handling.</li>
|
||||
</ul>
|
||||
<p>Links and text handling:</p>
|
||||
<ul>
|
||||
<li>Refined command-character parsing when pasting path-like text.</li>
|
||||
<li>Tightened topic link detection and opening rules.</li>
|
||||
<li>Added Public Suffix List validation for host links to reduce bad link matches.</li>
|
||||
</ul>
|
||||
<p>Packaging and platform integration:</p>
|
||||
<ul>
|
||||
<li>Sanitized the Linux open environment for AppImage builds and added safer fallback handling.</li>
|
||||
<li>Labeled Windows installer runtimes with exact versions.</li>
|
||||
<li>Fixed the notification icon and corrected the Flatpak screenshot asset.</li>
|
||||
<li>Improved AppStream metainfo validation.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-03-30" version="2.18.0~pre6">
|
||||
<description>
|
||||
<p>GTK theme and UI:</p>
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
"command": "zoitechat",
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--socket=fallback-x11",
|
||||
"--share=network",
|
||||
"--socket=pulseaudio",
|
||||
"--filesystem=xdg-download",
|
||||
"--filesystem=xdg-data/themes:ro",
|
||||
"--filesystem=xdg-data/icons:ro",
|
||||
"--filesystem=~/.themes:ro",
|
||||
"--filesystem=~/.icons:ro",
|
||||
"--filesystem=xdg-run/tray-icon:create",
|
||||
"--env=GTK_CSD=1",
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
@@ -34,7 +33,6 @@
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
"shared-modules/dbus-glib/dbus-glib.json",
|
||||
"shared-modules/lua5.4/lua-5.4.json",
|
||||
"shared-modules/libcanberra/libcanberra.json",
|
||||
"shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json",
|
||||
@@ -45,9 +43,9 @@
|
||||
"buildsystem": "meson",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/pavouk/lgi.git",
|
||||
"commit": "c9b8e4473c6421f2a215d8c06c0d94b86eb0b26a"
|
||||
"type": "archive",
|
||||
"url": "https://github.com/pavouk/lgi/archive/c9b8e4473c6421f2a215d8c06c0d94b86eb0b26a.tar.gz",
|
||||
"sha256": "db67b2b7ee89fa566f783486d56be7203552a997bc55f35020b57dd2776b9943"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -67,7 +65,8 @@
|
||||
"/share/man"
|
||||
],
|
||||
"post-install": [
|
||||
"install -d /app/extensions"
|
||||
"install -d /app/extensions",
|
||||
"test ! -e /app/share/fonts/zoitechat/NotoColorEmoji.ttf"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://www.cpan.org/src/5.0/perl-5.40.1.tar.xz",
|
||||
"sha256": "dfa20c2eef2b4af133525610bbb65dd13777ecf998c9c5b1ccf0d308e732ee3f"
|
||||
"url": "https://www.cpan.org/src/5.0/perl-5.42.2.tar.xz",
|
||||
"sha256": "0a585eeb9e363c0f80482ddb3571625250c2c86aeb408853e8ea50805cfb14bb"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
10
meson.build
10
meson.build
@@ -1,5 +1,5 @@
|
||||
project('zoitechat', 'c',
|
||||
version: '2.18.0~pre6',
|
||||
version: '2.18.3',
|
||||
meson_version: '>= 0.55.0',
|
||||
default_options: [
|
||||
'c_std=c17',
|
||||
@@ -18,7 +18,8 @@ libgmodule_dep = dependency('gmodule-2.0')
|
||||
|
||||
libcanberra_dep = dependency('libcanberra', version: '>= 0.22',
|
||||
required: get_option('libcanberra'))
|
||||
dbus_glib_dep = dependency('dbus-glib-1', required: get_option('dbus'))
|
||||
dbus_dep = dependency('gio-2.0', required: get_option('dbus'))
|
||||
libsecret_dep = dependency('libsecret-1', required: false)
|
||||
|
||||
global_deps = []
|
||||
if cc.get_id() == 'msvc'
|
||||
@@ -39,7 +40,8 @@ config_h.set10('ENABLE_NLS', true)
|
||||
# Optional features
|
||||
config_h.set('USE_OPENSSL', libssl_dep.found())
|
||||
config_h.set('USE_LIBCANBERRA', libcanberra_dep.found())
|
||||
config_h.set('USE_DBUS', dbus_glib_dep.found())
|
||||
config_h.set('USE_DBUS', dbus_dep.found())
|
||||
config_h.set('HAVE_LIBSECRET', libsecret_dep.found())
|
||||
config_h.set('USE_PLUGIN', get_option('plugin'))
|
||||
config_h.set('USE_GTK_FRONTEND', get_option('gtk-frontend'))
|
||||
|
||||
@@ -181,7 +183,7 @@ if meson.version().version_compare('>= 0.55.0')
|
||||
'GTK Frontend': get_option('gtk-frontend') ? 'enabled (GTK+ 3.22+)' : 'disabled',
|
||||
'TLS (openssl)': libssl_dep.found(),
|
||||
'Plugin Support': get_option('plugin'),
|
||||
'DBus Support': dbus_glib_dep.found(),
|
||||
'DBus Support': dbus_dep.found(),
|
||||
'libcanberra': libcanberra_dep.found(),
|
||||
}, section: 'Features')
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ option('with-exec', type: 'boolean',
|
||||
option('with-fishlim', type: 'boolean',
|
||||
description: 'Fish encryption plugin, requires openssl'
|
||||
)
|
||||
option('with-lua', type: 'string', value: 'luajit',
|
||||
option('with-lua', type: 'string', value: 'lua-5.4',
|
||||
description: 'Lua scripting plugin, value is pkg-config name to use or "false"'
|
||||
)
|
||||
option('with-perl', type: 'string', value: 'perl',
|
||||
@@ -59,9 +59,6 @@ option('with-sysinfo', type: 'boolean',
|
||||
option('with-upd', type: 'boolean',
|
||||
description: 'Update plugin, Windows only'
|
||||
)
|
||||
option('with-winamp', type: 'boolean',
|
||||
description: 'Winamp plugin, Windows only'
|
||||
)
|
||||
option('with-perl-legacy-api', type: 'boolean', value: false,
|
||||
description: 'Enables the legacy IRC perl module for compatibility with old scripts'
|
||||
)
|
||||
|
||||
103
packaging/fedora/zoitechat.spec
Normal file
103
packaging/fedora/zoitechat.spec
Normal file
@@ -0,0 +1,103 @@
|
||||
Name: zoitechat
|
||||
Version: 2.18.3
|
||||
Release: %autorelease
|
||||
Summary: HexChat-based IRC client
|
||||
License: GPL-2.0-or-later WITH cryptsetup-OpenSSL-exception
|
||||
URL: https://github.com/ZoiteChat/zoitechat
|
||||
Source0: %{url}/archive/refs/tags/%{name}-%{version}.tar.gz
|
||||
|
||||
BuildRequires: desktop-file-utils
|
||||
BuildRequires: gcc
|
||||
BuildRequires: gettext
|
||||
BuildRequires: libappstream-glib
|
||||
BuildRequires: meson >= 0.55.0
|
||||
BuildRequires: perl
|
||||
BuildRequires: perl-devel
|
||||
BuildRequires: python3
|
||||
BuildRequires: python3-cffi
|
||||
BuildRequires: publicsuffix-list
|
||||
BuildRequires: xwayland-run
|
||||
BuildRequires: weston
|
||||
BuildRequires: pkgconfig(ayatana-appindicator3-0.1)
|
||||
BuildRequires: pkgconfig(dbus-glib-1)
|
||||
BuildRequires: pkgconfig(gio-2.0) >= 2.36.0
|
||||
BuildRequires: pkgconfig(gmodule-2.0)
|
||||
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
|
||||
BuildRequires: pkgconfig(iso-codes)
|
||||
BuildRequires: pkgconfig(libarchive)
|
||||
BuildRequires: pkgconfig(libcanberra) >= 0.22
|
||||
BuildRequires: pkgconfig(libpci)
|
||||
BuildRequires: pkgconfig(lua)
|
||||
BuildRequires: pkgconfig(openssl) >= 0.9.8
|
||||
BuildRequires: pkgconfig(python3)
|
||||
|
||||
Requires: google-noto-color-emoji-fonts
|
||||
Requires: hicolor-icon-theme
|
||||
Requires: iso-codes
|
||||
|
||||
%package devel
|
||||
Summary: Development files for ZoiteChat plugins
|
||||
Requires: %{name}%{?_isa} = %{version}-%{release}
|
||||
|
||||
%description devel
|
||||
Development files for building ZoiteChat plugins.
|
||||
|
||||
%description
|
||||
ZoiteChat is a HexChat-based IRC client for Windows and UNIX-like operating
|
||||
systems.
|
||||
|
||||
%prep
|
||||
%autosetup -C
|
||||
|
||||
%build
|
||||
%meson \
|
||||
-Dtext-frontend=false \
|
||||
-Dwith-checksum=true \
|
||||
-Dwith-fishlim=true \
|
||||
-Dwith-lua=lua \
|
||||
-Dwith-perl=perl \
|
||||
-Dwith-python=python3 \
|
||||
-Dwith-sysinfo=true \
|
||||
-Dinstall-appdata=true \
|
||||
-Dinstall-plugin-metainfo=true
|
||||
%meson_build
|
||||
|
||||
%install
|
||||
%meson_install
|
||||
%find_lang %{name}
|
||||
|
||||
%check
|
||||
desktop-file-validate %{buildroot}%{_datadir}/applications/net.zoite.Zoitechat.desktop
|
||||
|
||||
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite.Zoitechat.appdata.xml
|
||||
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
|
||||
|
||||
xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_build_ncpus} --print-errorlogs \
|
||||
"Theme Manager Dispatch Routing Tests" \
|
||||
"Validate net.zoite.Zoitechat.desktop" \
|
||||
"Validate translations" \
|
||||
"Fishlim Tests"
|
||||
|
||||
%files -f %{name}.lang
|
||||
%license COPYING
|
||||
%doc readme.md troubleshooting.md
|
||||
%{_bindir}/zoitechat
|
||||
%{_datadir}/applications/net.zoite.Zoitechat.desktop
|
||||
%{_datadir}/dbus-1/services/org.zoitechat.service.service
|
||||
%{_datadir}/icons/hicolor/48x48/apps/net.zoite.Zoitechat.png
|
||||
%{_datadir}/icons/hicolor/scalable/apps/net.zoite.Zoitechat.svg
|
||||
%{_datadir}/metainfo/net.zoite.Zoitechat.appdata.xml
|
||||
%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
|
||||
%dir %{_libdir}/zoitechat
|
||||
%dir %{_libdir}/zoitechat/plugins
|
||||
%dir %{_libdir}/zoitechat/python
|
||||
%{_libdir}/zoitechat/plugins/*.so
|
||||
%{_libdir}/zoitechat/python/*.py
|
||||
%{_mandir}/man1/zoitechat.1*
|
||||
|
||||
%files devel
|
||||
%{_includedir}/zoitechat-plugin.h
|
||||
%{_libdir}/pkgconfig/zoitechat-plugin.pc
|
||||
|
||||
%changelog
|
||||
%autochangelog
|
||||
@@ -1,70 +0,0 @@
|
||||
# Maintainer: ZoiteChat Maintainers <zoitechat@users.noreply.github.com>
|
||||
|
||||
pkgname=zoitechat-git
|
||||
pkgver=0
|
||||
pkgrel=1
|
||||
pkgdesc='IRC client for GNOME (ZoiteChat development snapshot)'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/zoitechat/zoitechat'
|
||||
license=('GPL-2.0-or-later')
|
||||
depends=(
|
||||
'dbus-glib'
|
||||
'glib2'
|
||||
'gtk3'
|
||||
'iso-codes'
|
||||
'libayatana-appindicator'
|
||||
'libcanberra'
|
||||
'lua'
|
||||
'openssl'
|
||||
'perl'
|
||||
'python-cffi'
|
||||
)
|
||||
makedepends=(
|
||||
'git'
|
||||
'meson'
|
||||
'ninja'
|
||||
'pkgconf'
|
||||
'python'
|
||||
)
|
||||
optdepends=(
|
||||
'pciutils: sysinfo plugin hardware detection details'
|
||||
)
|
||||
provides=('zoitechat')
|
||||
conflicts=('zoitechat')
|
||||
|
||||
_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source=("zoitechat::git+file://${_repo_root}")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "$srcdir/zoitechat"
|
||||
|
||||
if git describe --long --tags --abbrev=7 >/dev/null 2>&1; then
|
||||
git describe --long --tags --abbrev=7 \
|
||||
| sed 's/^v//' \
|
||||
| sed -E 's/([^-]+)-([0-9]+)-g/\1.r\2.g/' \
|
||||
| sed 's/-/./g'
|
||||
else
|
||||
printf '0.r%s.%s\n' \
|
||||
"$(git rev-list --count HEAD)" \
|
||||
"$(git rev-parse --short=7 HEAD)"
|
||||
fi
|
||||
}
|
||||
|
||||
build() {
|
||||
arch-meson zoitechat build \
|
||||
-Dtext-frontend=false \
|
||||
-Dwith-checksum=true \
|
||||
-Dwith-fishlim=true \
|
||||
-Dwith-lua=lua \
|
||||
-Dwith-perl=perl \
|
||||
-Dwith-python=python3 \
|
||||
-Dwith-sysinfo=true \
|
||||
-Dinstall-plugin-metainfo=true
|
||||
|
||||
meson compile -C build
|
||||
}
|
||||
|
||||
package() {
|
||||
meson install -C build --destdir "$pkgdir"
|
||||
}
|
||||
@@ -104,12 +104,14 @@ thread_sha256_file (GTask *task, GFile *file, gpointer task_data, GCancellable *
|
||||
g_checksum_update (checksum, buffer, ret);
|
||||
|
||||
if (error) {
|
||||
g_checksum_free (checksum);
|
||||
g_task_return_error (task, error);
|
||||
return;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
g_task_return_pointer (task, g_strdup (g_checksum_get_string (checksum)), g_free);
|
||||
cleanup:
|
||||
g_input_stream_close(G_INPUT_STREAM(istream), NULL, NULL);
|
||||
g_object_unref(istream);
|
||||
g_checksum_free (checksum);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,27 +91,13 @@ static const signed char fish_unbase64[256] = {
|
||||
#include <openssl/provider.h>
|
||||
static OSSL_PROVIDER *legacy_provider;
|
||||
static OSSL_PROVIDER *default_provider;
|
||||
static OSSL_LIB_CTX *ossl_ctx;
|
||||
#endif
|
||||
|
||||
int fish_init(void)
|
||||
{
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
ossl_ctx = OSSL_LIB_CTX_new();
|
||||
if (!ossl_ctx)
|
||||
return 0;
|
||||
|
||||
legacy_provider = OSSL_PROVIDER_load(ossl_ctx, "legacy");
|
||||
if (!legacy_provider) {
|
||||
fish_deinit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
default_provider = OSSL_PROVIDER_load(ossl_ctx, "default");
|
||||
if (!default_provider) {
|
||||
fish_deinit();
|
||||
return 0;
|
||||
}
|
||||
legacy_provider = OSSL_PROVIDER_load(NULL, "legacy");
|
||||
default_provider = OSSL_PROVIDER_load(NULL, "default");
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
@@ -129,10 +115,6 @@ void fish_deinit(void)
|
||||
default_provider = NULL;
|
||||
}
|
||||
|
||||
if (ossl_ctx) {
|
||||
OSSL_LIB_CTX_free(ossl_ctx);
|
||||
ossl_ctx = NULL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -278,7 +260,9 @@ char *fish_cipher(const char *plaintext, size_t plaintext_len, const char *key,
|
||||
}
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
cipher = EVP_CIPHER_fetch(ossl_ctx, "BF-CBC", NULL);
|
||||
cipher = EVP_CIPHER_fetch(NULL, "BF-CBC", NULL);
|
||||
if (!cipher)
|
||||
cipher = (EVP_CIPHER *) EVP_bf_cbc();
|
||||
#else
|
||||
cipher = (EVP_CIPHER *) EVP_bf_cbc();
|
||||
#endif
|
||||
@@ -286,7 +270,9 @@ char *fish_cipher(const char *plaintext, size_t plaintext_len, const char *key,
|
||||
} else if (mode == EVP_CIPH_ECB_MODE) {
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
cipher = EVP_CIPHER_fetch(ossl_ctx, "BF-ECB", NULL);
|
||||
cipher = EVP_CIPHER_fetch(NULL, "BF-ECB", NULL);
|
||||
if (!cipher)
|
||||
cipher = (EVP_CIPHER *) EVP_bf_ecb();
|
||||
#else
|
||||
cipher = (EVP_CIPHER *) EVP_bf_ecb();
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;HAVE_DH_SET0_PQG;HAVE_DH_GET0_KEY;HAVE_DH_SET0_KEY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(OpenSslInclude);$(Glib);..\..\src\common;$(ZoiteChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);..\..\src\common;$(ZoiteChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<ModuleDefinitionFile>fishlim.def</ModuleDefinitionFile>
|
||||
|
||||
@@ -194,6 +194,18 @@ static gboolean keyfile_save_to_file (GKeyFile *keyfile, char *filename) {
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
gchar **keystore_get_targets(gsize *length) {
|
||||
GKeyFile *keyfile;
|
||||
gchar **groups;
|
||||
|
||||
keyfile = getConfigFile();
|
||||
groups = g_key_file_get_groups(keyfile, length);
|
||||
g_key_file_free(keyfile);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the key store file to disk.
|
||||
*/
|
||||
@@ -217,6 +229,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
/**
|
||||
* Sets a key in the key store file.
|
||||
*/
|
||||
|
||||
gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode) {
|
||||
const char *password;
|
||||
char *encrypted;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
char *keystore_get_key(const char *nick, enum fish_mode *mode);
|
||||
gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode);
|
||||
gboolean keystore_delete_nick(const char *nick);
|
||||
gchar **keystore_get_targets(gsize *length);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ fishlim_sources = [
|
||||
]
|
||||
|
||||
shared_module('fishlim', fishlim_sources,
|
||||
dependencies: [libgio_dep, zoitechat_plugin_dep, libssl_dep],
|
||||
dependencies: [libgio_dep, gtk_dep, zoitechat_plugin_dep, libssl_dep],
|
||||
c_args: ['-DOPENSSL_API_COMPAT=0x10100000L'],
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "zoitechat-plugin.h"
|
||||
@@ -44,7 +45,7 @@ static const char plugin_version[] = "1.0.0";
|
||||
|
||||
static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] [<mode>:]<password>, sets the key for a channel or nick. Modes: ECB, CBC";
|
||||
static const char usage_delkey[] = "Usage: DELKEY [<nick or #channel>], deletes the key for a channel or nick";
|
||||
static const char usage_keyx[] = "Usage: KEYX [<nick>], performs DH1080 key-exchange with <nick>";
|
||||
static const char usage_keyx[] = "Usage: KEYX [<nick>] [ECB|CBC], performs DH1080 key-exchange with <nick>";
|
||||
static const char usage_topic[] = "Usage: TOPIC+ <topic>, sets a new encrypted topic for the current channel";
|
||||
static const char usage_notice[] = "Usage: NOTICE+ <nick or #channel> <notice>";
|
||||
static const char usage_msg[] = "Usage: MSG+ <nick or #channel> <message>";
|
||||
@@ -52,6 +53,13 @@ static const char usage_msg[] = "Usage: MSG+ <nick or #channel> <message>";
|
||||
|
||||
static zoitechat_plugin *ph;
|
||||
static GHashTable *pending_exchanges;
|
||||
static GtkWidget *fishlim_dialog;
|
||||
static GtkWidget *fishlim_target_entry;
|
||||
static GtkWidget *fishlim_key_entry;
|
||||
static GtkWidget *fishlim_mode_combo;
|
||||
static GtkWidget *fishlim_status_label;
|
||||
static GtkListStore *fishlim_store;
|
||||
static GtkWidget *fishlim_view;
|
||||
|
||||
|
||||
/**
|
||||
@@ -421,7 +429,7 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
zoitechat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s%s", sender, pub_key, (mode == FISH_CBC_MODE) ? " CBC" : "");
|
||||
g_free(pub_key);
|
||||
} else {
|
||||
zoitechat_print(ph, "Failed to generate keys");
|
||||
zoitechat_printf(ph, "Failed to generate keys");
|
||||
goto cleanup;
|
||||
}
|
||||
} else if (!strcmp (dh_message, "DH1080_FINISH")) {
|
||||
@@ -446,7 +454,7 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
zoitechat_printf(ph, "Stored new key for %s (%s)", sender, fish_modes[mode]);
|
||||
g_free(secret_key);
|
||||
} else {
|
||||
zoitechat_print(ph, "Failed to create secret key!");
|
||||
zoitechat_printf(ph, "Failed to create secret key!");
|
||||
}
|
||||
|
||||
cleanup:
|
||||
@@ -455,6 +463,314 @@ cleanup:
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
|
||||
static const char *fishlim_gui_target(void) {
|
||||
const char *target;
|
||||
|
||||
target = gtk_entry_get_text(GTK_ENTRY(fishlim_target_entry));
|
||||
if (target && *target)
|
||||
return target;
|
||||
|
||||
return zoitechat_get_info(ph, "channel");
|
||||
}
|
||||
|
||||
static void fishlim_gui_refresh(void) {
|
||||
GtkTreeIter iter;
|
||||
gchar **targets;
|
||||
gsize length, i;
|
||||
const char *target;
|
||||
char *key;
|
||||
enum fish_mode mode;
|
||||
|
||||
if (!fishlim_dialog)
|
||||
return;
|
||||
|
||||
gtk_list_store_clear(fishlim_store);
|
||||
targets = keystore_get_targets(&length);
|
||||
|
||||
for (i = 0; targets && i < length; i++) {
|
||||
key = keystore_get_key(targets[i], &mode);
|
||||
gtk_list_store_append(fishlim_store, &iter);
|
||||
gtk_list_store_set(fishlim_store, &iter, 0, targets[i], 1, fish_modes[mode], 2, key ? "Stored" : "Unreadable", -1);
|
||||
g_free(key);
|
||||
}
|
||||
|
||||
g_strfreev(targets);
|
||||
|
||||
target = fishlim_gui_target();
|
||||
key = target ? keystore_get_key(target, &mode) : NULL;
|
||||
|
||||
if (key) {
|
||||
char *text = g_strdup_printf("Key stored for %s (%s)", target, fish_modes[mode]);
|
||||
gtk_label_set_text(GTK_LABEL(fishlim_status_label), text);
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(fishlim_mode_combo), mode == FISH_CBC_MODE ? 1 : 0);
|
||||
g_free(text);
|
||||
g_free(key);
|
||||
} else if (target && *target) {
|
||||
char *text = g_strdup_printf("No key for %s", target);
|
||||
gtk_label_set_text(GTK_LABEL(fishlim_status_label), text);
|
||||
g_free(text);
|
||||
} else {
|
||||
gtk_label_set_text(GTK_LABEL(fishlim_status_label), "No target selected");
|
||||
}
|
||||
}
|
||||
|
||||
static void fishlim_gui_refresh_cb(GtkWidget *widget, gpointer data) {
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
static void fishlim_gui_send_channel_key(const char *channel, const char *mode, const char *key) {
|
||||
GtkWidget *dialog, *content, *entry, *label;
|
||||
char **users;
|
||||
int i;
|
||||
|
||||
if (!channel || !*channel || irc_is_query(channel))
|
||||
return;
|
||||
|
||||
dialog = gtk_dialog_new_with_buttons("Share Channel Key", GTK_WINDOW(fishlim_dialog), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "_Skip", GTK_RESPONSE_CANCEL, "_Send", GTK_RESPONSE_OK, NULL);
|
||||
content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||
label = gtk_label_new("Send this channel key to users by private message. Enter users separated by commas.");
|
||||
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(content), label, FALSE, FALSE, 6);
|
||||
entry = gtk_entry_new();
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "nick1, nick2, nick3");
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(content), entry, FALSE, FALSE, 6);
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK);
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
|
||||
users = g_strsplit(gtk_entry_get_text(GTK_ENTRY(entry)), ",", -1);
|
||||
for (i = 0; users && users[i]; i++) {
|
||||
char *user = g_strstrip(users[i]);
|
||||
if (*user)
|
||||
zoitechat_commandf(ph, "msg %s Encrypted chat invite: join %s, open FiSHLiM Key Manager, add channel %s with %s key: %s. Key exchange is for private messages only.", user, channel, channel, mode, key);
|
||||
}
|
||||
g_strfreev(users);
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
}
|
||||
|
||||
static void fishlim_gui_set(GtkWidget *widget, gpointer data) {
|
||||
const char *target = fishlim_gui_target();
|
||||
const char *key = gtk_entry_get_text(GTK_ENTRY(fishlim_key_entry));
|
||||
const char *mode = gtk_combo_box_get_active(GTK_COMBO_BOX(fishlim_mode_combo)) == 1 ? "CBC" : "ECB";
|
||||
|
||||
if (!target || !*target || !key || !*key) {
|
||||
zoitechat_printf(ph, "%s\n", usage_setkey);
|
||||
return;
|
||||
}
|
||||
|
||||
zoitechat_commandf(ph, "SETKEY %s %s:%s", target, mode, key);
|
||||
fishlim_gui_send_channel_key(target, mode, key);
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_key_entry), "");
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
static gboolean fishlim_gui_confirm_delete(const char *target) {
|
||||
GtkWidget *dialog;
|
||||
char *message;
|
||||
gboolean confirmed;
|
||||
|
||||
message = g_strdup_printf("Delete the key for %s?", target);
|
||||
dialog = gtk_message_dialog_new(GTK_WINDOW(fishlim_dialog), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_WARNING, GTK_BUTTONS_NONE, "%s", message);
|
||||
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "This cannot be undone.");
|
||||
gtk_dialog_add_buttons(GTK_DIALOG(dialog), "_Cancel", GTK_RESPONSE_CANCEL, "_Delete", GTK_RESPONSE_ACCEPT, NULL);
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
|
||||
confirmed = gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT;
|
||||
gtk_widget_destroy(dialog);
|
||||
g_free(message);
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
static char *fishlim_gui_selected_target(void) {
|
||||
GtkTreeSelection *selection;
|
||||
GtkTreeModel *model;
|
||||
GtkTreeIter iter;
|
||||
char *target = NULL;
|
||||
|
||||
if (!fishlim_view)
|
||||
return NULL;
|
||||
|
||||
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(fishlim_view));
|
||||
if (gtk_tree_selection_get_selected(selection, &model, &iter))
|
||||
gtk_tree_model_get(model, &iter, 0, &target, -1);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
static void fishlim_gui_delete(GtkWidget *widget, gpointer data) {
|
||||
char *selected = fishlim_gui_selected_target();
|
||||
const char *target = selected ? selected : fishlim_gui_target();
|
||||
|
||||
if (!target || !*target) {
|
||||
zoitechat_printf(ph, "%s\n", usage_delkey);
|
||||
g_free(selected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fishlim_gui_confirm_delete(target)) {
|
||||
zoitechat_commandf(ph, "DELKEY %s", target);
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
g_free(selected);
|
||||
}
|
||||
|
||||
static char *fishlim_gui_prompt_target(const char *title, const char *initial) {
|
||||
GtkWidget *dialog, *content, *entry;
|
||||
char *target = NULL;
|
||||
|
||||
dialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(fishlim_dialog), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "_Cancel", GTK_RESPONSE_CANCEL, "_OK", GTK_RESPONSE_OK, NULL);
|
||||
content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||
entry = gtk_entry_new();
|
||||
if (initial && *initial)
|
||||
gtk_entry_set_text(GTK_ENTRY(entry), initial);
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(content), gtk_label_new("Private message user"), FALSE, FALSE, 6);
|
||||
gtk_box_pack_start(GTK_BOX(content), entry, FALSE, FALSE, 6);
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK);
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
|
||||
const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
|
||||
if (text && *text)
|
||||
target = g_strdup(text);
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
return target;
|
||||
}
|
||||
|
||||
static void fishlim_gui_keyx(GtkWidget *widget, gpointer data) {
|
||||
char *target;
|
||||
const char *initial = fishlim_gui_target();
|
||||
const char *mode = gtk_combo_box_get_active(GTK_COMBO_BOX(fishlim_mode_combo)) == 1 ? "CBC" : "ECB";
|
||||
|
||||
if (!initial || !*initial || !irc_is_query(initial))
|
||||
initial = NULL;
|
||||
|
||||
target = fishlim_gui_prompt_target("Key Exchange", initial);
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
zoitechat_commandf(ph, "KEYX %s %s", target, mode);
|
||||
g_free(target);
|
||||
}
|
||||
|
||||
static void fishlim_gui_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *column, gpointer data) {
|
||||
GtkTreeModel *model = gtk_tree_view_get_model(tree);
|
||||
GtkTreeIter iter;
|
||||
char *target;
|
||||
|
||||
if (!gtk_tree_model_get_iter(model, &iter, path))
|
||||
return;
|
||||
|
||||
gtk_tree_model_get(model, &iter, 0, &target, -1);
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||
g_free(target);
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
static void fishlim_gui_destroy(GtkWidget *widget, gpointer data) {
|
||||
fishlim_dialog = NULL;
|
||||
fishlim_target_entry = NULL;
|
||||
fishlim_key_entry = NULL;
|
||||
fishlim_mode_combo = NULL;
|
||||
fishlim_status_label = NULL;
|
||||
fishlim_store = NULL;
|
||||
fishlim_view = NULL;
|
||||
}
|
||||
|
||||
static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
||||
GtkWidget *content, *grid, *view, *scroll, *buttons, *button;
|
||||
GtkCellRenderer *renderer;
|
||||
const char *target;
|
||||
|
||||
target = *word_eol[2] ? word_eol[2] : zoitechat_get_info(ph, "channel");
|
||||
|
||||
if (fishlim_dialog) {
|
||||
if (target)
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||
gtk_window_present(GTK_WINDOW(fishlim_dialog));
|
||||
return ZOITECHAT_EAT_ZOITECHAT;
|
||||
}
|
||||
|
||||
fishlim_dialog = gtk_dialog_new_with_buttons("FiSHLiM Key Manager", NULL, GTK_DIALOG_DESTROY_WITH_PARENT, "_Close", GTK_RESPONSE_CLOSE, NULL);
|
||||
gtk_window_set_default_size(GTK_WINDOW(fishlim_dialog), 520, 360);
|
||||
g_signal_connect(fishlim_dialog, "response", G_CALLBACK(gtk_widget_destroy), NULL);
|
||||
g_signal_connect(fishlim_dialog, "destroy", G_CALLBACK(fishlim_gui_destroy), NULL);
|
||||
|
||||
content = gtk_dialog_get_content_area(GTK_DIALOG(fishlim_dialog));
|
||||
grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
|
||||
gtk_container_set_border_width(GTK_CONTAINER(grid), 12);
|
||||
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
|
||||
|
||||
fishlim_target_entry = gtk_entry_new();
|
||||
if (target)
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target nick or channel"), 0, 0, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_target_entry, 1, 0, 2, 1);
|
||||
|
||||
fishlim_mode_combo = gtk_combo_box_text_new();
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(fishlim_mode_combo), "ECB");
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(fishlim_mode_combo), "CBC");
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(fishlim_mode_combo), 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Mode"), 0, 1, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_mode_combo, 1, 1, 2, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("CBC may not work with older clients. Key exchange is private-message only."), 1, 2, 2, 1);
|
||||
|
||||
fishlim_key_entry = gtk_entry_new();
|
||||
gtk_entry_set_visibility(GTK_ENTRY(fishlim_key_entry), FALSE);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Key"), 0, 3, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_key_entry, 1, 3, 2, 1);
|
||||
|
||||
buttons = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_button_box_set_layout(GTK_BUTTON_BOX(buttons), GTK_BUTTONBOX_END);
|
||||
gtk_grid_attach(GTK_GRID(grid), buttons, 0, 4, 3, 1);
|
||||
|
||||
button = gtk_button_new_with_label("Set Key");
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(fishlim_gui_set), NULL);
|
||||
gtk_container_add(GTK_CONTAINER(buttons), button);
|
||||
|
||||
button = gtk_button_new_with_label("Delete Key");
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(fishlim_gui_delete), NULL);
|
||||
gtk_container_add(GTK_CONTAINER(buttons), button);
|
||||
|
||||
button = gtk_button_new_with_label("Private Message Key Exchange");
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(fishlim_gui_keyx), NULL);
|
||||
gtk_container_add(GTK_CONTAINER(buttons), button);
|
||||
|
||||
fishlim_status_label = gtk_label_new(NULL);
|
||||
gtk_label_set_xalign(GTK_LABEL(fishlim_status_label), 0.0);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_status_label, 0, 5, 3, 1);
|
||||
|
||||
fishlim_store = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
||||
view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(fishlim_store));
|
||||
fishlim_view = view;
|
||||
renderer = gtk_cell_renderer_text_new();
|
||||
gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), -1, "Target", renderer, "text", 0, NULL);
|
||||
renderer = gtk_cell_renderer_text_new();
|
||||
gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), -1, "Mode", renderer, "text", 1, NULL);
|
||||
renderer = gtk_cell_renderer_text_new();
|
||||
gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), -1, "Status", renderer, "text", 2, NULL);
|
||||
g_signal_connect(view, "row-activated", G_CALLBACK(fishlim_gui_row_activated), NULL);
|
||||
|
||||
scroll = gtk_scrolled_window_new(NULL, NULL);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_container_add(GTK_CONTAINER(scroll), view);
|
||||
gtk_grid_attach(GTK_GRID(grid), scroll, 0, 6, 3, 1);
|
||||
|
||||
g_signal_connect(fishlim_target_entry, "changed", G_CALLBACK(fishlim_gui_refresh_cb), NULL);
|
||||
fishlim_gui_refresh();
|
||||
|
||||
gtk_widget_show_all(fishlim_dialog);
|
||||
return ZOITECHAT_EAT_ZOITECHAT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler for /setkey
|
||||
*/
|
||||
@@ -490,6 +806,8 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
/* Set password */
|
||||
if (keystore_store_key(nick, key, mode)) {
|
||||
zoitechat_printf(ph, "Stored key for %s (%s)\n", nick, fish_modes[mode]);
|
||||
if (mode == FISH_CBC_MODE)
|
||||
zoitechat_printf(ph, "Warning: CBC may not work with older clients.\n");
|
||||
} else {
|
||||
zoitechat_printf(ph, "\00305Failed to store key in addon_fishlim.conf\n");
|
||||
}
|
||||
@@ -533,8 +851,18 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target = word[2];
|
||||
zoitechat_context *query_ctx = NULL;
|
||||
char *pub_key, *priv_key;
|
||||
enum fish_mode mode = FISH_CBC_MODE;
|
||||
int ctx_type;
|
||||
|
||||
if (*word[3]) {
|
||||
if (g_ascii_strcasecmp(word[3], "ECB") == 0)
|
||||
mode = FISH_ECB_MODE;
|
||||
else if (g_ascii_strcasecmp(word[3], "CBC") != 0) {
|
||||
zoitechat_printf(ph, "%s", usage_keyx);
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
}
|
||||
|
||||
if (*target)
|
||||
query_ctx = find_context_on_network(target);
|
||||
else {
|
||||
@@ -548,19 +876,21 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
}
|
||||
|
||||
if ((query_ctx && ctx_type != 3) || (!query_ctx && !irc_is_query(target))) {
|
||||
zoitechat_print(ph, "You can only exchange keys with individuals");
|
||||
zoitechat_printf(ph, "You can only exchange keys with individuals");
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
if (dh1080_generate_key(&priv_key, &pub_key)) {
|
||||
g_hash_table_replace (pending_exchanges, g_ascii_strdown(target, -1), priv_key);
|
||||
|
||||
zoitechat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s CBC", target, pub_key);
|
||||
zoitechat_printf(ph, "Sent public key to %s (CBC), waiting for reply...", target);
|
||||
zoitechat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s%s", target, pub_key, (mode == FISH_CBC_MODE) ? " CBC" : "");
|
||||
zoitechat_printf(ph, "Sent public key to %s (%s), waiting for reply...", target, fish_modes[mode]);
|
||||
if (mode == FISH_CBC_MODE)
|
||||
zoitechat_printf(ph, "Warning: CBC may not work with older clients.");
|
||||
|
||||
g_free(pub_key);
|
||||
} else {
|
||||
zoitechat_print(ph, "Failed to generate keys");
|
||||
zoitechat_printf(ph, "Failed to generate keys");
|
||||
}
|
||||
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
@@ -577,7 +907,7 @@ static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) {
|
||||
GSList *encrypted_list;
|
||||
|
||||
if (!*topic) {
|
||||
zoitechat_print(ph, usage_topic);
|
||||
zoitechat_printf(ph, "%s", usage_topic);
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
@@ -624,7 +954,7 @@ static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
if (!*target || !*notice) {
|
||||
zoitechat_print(ph, usage_notice);
|
||||
zoitechat_printf(ph, "%s", usage_notice);
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
@@ -676,7 +1006,7 @@ static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) {
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
if (!*target || !*message) {
|
||||
zoitechat_print(ph, usage_msg);
|
||||
zoitechat_printf(ph, "%s", usage_msg);
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
@@ -789,6 +1119,7 @@ int zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
|
||||
*version = plugin_version;
|
||||
|
||||
/* Register commands */
|
||||
zoitechat_hook_command(ph, "FISHLIM", ZOITECHAT_PRI_NORM, handle_fishlim, "Usage: FISHLIM, opens the FiSHLiM key manager", NULL);
|
||||
zoitechat_hook_command(ph, "SETKEY", ZOITECHAT_PRI_NORM, handle_setkey, usage_setkey, NULL);
|
||||
zoitechat_hook_command(ph, "DELKEY", ZOITECHAT_PRI_NORM, handle_delkey, usage_delkey, NULL);
|
||||
zoitechat_hook_command(ph, "KEYX", ZOITECHAT_PRI_NORM, handle_keyx, usage_keyx, NULL);
|
||||
@@ -805,20 +1136,31 @@ int zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
|
||||
zoitechat_hook_server_attrs(ph, "TOPIC", ZOITECHAT_PRI_NORM, handle_incoming, NULL);
|
||||
zoitechat_hook_server_attrs(ph, "332", ZOITECHAT_PRI_NORM, handle_incoming, NULL);
|
||||
|
||||
if (!fish_init())
|
||||
if (!fish_init()) {
|
||||
zoitechat_printf(ph, "FiSHLiM failed to initialize crypto backend");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!dh1080_init())
|
||||
if (!dh1080_init()) {
|
||||
zoitechat_printf(ph, "FiSHLiM failed to initialize DH1080");
|
||||
return 0;
|
||||
}
|
||||
|
||||
pending_exchanges = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
|
||||
zoitechat_command(ph, "MENU ADD \"Window/FiSHLiM Key Manager\" \"FISHLIM\"");
|
||||
zoitechat_command(ph, "MENU ADD \"$NICK/FiSHLiM Key Manager\" \"FISHLIM %s\"");
|
||||
|
||||
zoitechat_printf(ph, "%s plugin loaded\n", plugin_name);
|
||||
/* Return success */
|
||||
return 1;
|
||||
}
|
||||
|
||||
int zoitechat_plugin_deinit(void) {
|
||||
zoitechat_command(ph, "MENU DEL \"Window/FiSHLiM Key Manager\"");
|
||||
zoitechat_command(ph, "MENU DEL \"$NICK/FiSHLiM Key Manager\"");
|
||||
if (fishlim_dialog)
|
||||
gtk_widget_destroy(fishlim_dialog);
|
||||
g_clear_pointer(&pending_exchanges, g_hash_table_destroy);
|
||||
dh1080_deinit();
|
||||
fish_deinit();
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
if cc.get_id() == 'msvc'
|
||||
lua_dep = cc.find_library('lua51')
|
||||
else
|
||||
lua_dep = dependency(get_option('with-lua'))
|
||||
lua_opt = get_option('with-lua')
|
||||
lua_dep = dependency(lua_opt, required: false)
|
||||
if not lua_dep.found() and lua_opt == 'lua-5.4'
|
||||
foreach lua_name : ['lua5.4', 'lua-5.3', 'lua5.3', 'lua']
|
||||
lua_dep = dependency(lua_name, required: false)
|
||||
if lua_dep.found()
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not lua_dep.found()
|
||||
error('Dependency "' + lua_opt + '" not found')
|
||||
endif
|
||||
endif
|
||||
|
||||
shared_module('lua', 'lua.c',
|
||||
|
||||
@@ -9,9 +9,6 @@ if host_machine.system() == 'windows'
|
||||
subdir('upd')
|
||||
endif
|
||||
|
||||
if get_option('with-winamp')
|
||||
subdir('winamp')
|
||||
endif
|
||||
endif
|
||||
|
||||
if get_option('with-checksum')
|
||||
|
||||
@@ -53,6 +53,16 @@ builder.set_source('_zoitechat_embedded', '''
|
||||
#include "config.h"
|
||||
#include "zoitechat-plugin.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
static zoitechat_plugin *ph;
|
||||
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||
CFFI_DLLEXPORT int _on_plugin_deinit(void);
|
||||
@@ -61,6 +71,20 @@ int zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
|
||||
char **name_out, char **description_out,
|
||||
char **version_out, char *arg)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
/* CPython cannot be removed from a process once Py_Initialize has run.
|
||||
If the embedded interpreter fails to start (e.g. _cffi_backend is
|
||||
missing), the host responds to our 0 return by g_module_close()ing
|
||||
this plugin, which would also drop the last reference to pythonXY.dll
|
||||
and unload it mid-flight; the leftover process-global state then
|
||||
crashes the process later, e.g. when a server connection spawns the
|
||||
next thread. Pin this module so FreeLibrary can never unload it. */
|
||||
HMODULE self_handle;
|
||||
GetModuleHandleExW (GET_MODULE_HANDLE_EX_FLAG_PIN |
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
|
||||
(LPCWSTR)&zoitechat_plugin_init, &self_handle);
|
||||
#endif
|
||||
|
||||
if (ph != NULL)
|
||||
{
|
||||
puts ("Python plugin already loaded\\n");
|
||||
|
||||
@@ -19,7 +19,7 @@ else:
|
||||
if not hasattr(sys, 'argv'):
|
||||
sys.argv = ['<zoitechat>']
|
||||
|
||||
VERSION = b'2.18.0~pre6'
|
||||
VERSION = b'2.18.3'
|
||||
PLUGIN_NAME = ffi.new('char[]', b'Python')
|
||||
PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1]))
|
||||
PLUGIN_VERSION = ffi.new('char[]', VERSION)
|
||||
@@ -173,7 +173,7 @@ class Plugin:
|
||||
|
||||
def __del__(self):
|
||||
log('unloading', self.filename)
|
||||
for hook in self.hooks:
|
||||
for hook in list(self.hooks):
|
||||
if hook.is_unload is True:
|
||||
try:
|
||||
hook.callback(hook.userdata)
|
||||
@@ -320,9 +320,9 @@ def _on_say_command(word, word_eol, userdata):
|
||||
return 0
|
||||
|
||||
try:
|
||||
python = _cstr(word_eol[1])
|
||||
python = __decode(_cstr(word_eol[1]))
|
||||
except Exception:
|
||||
python = b''
|
||||
python = ''
|
||||
|
||||
if not python:
|
||||
return 1
|
||||
|
||||
64
plugins/python/python38.vcxproj
Normal file
64
plugins/python/python38.vcxproj
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{D58924D2-1492-4AC3-B058-7A214424EEDC}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>python38</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="..\..\win32\zoitechat.props" />
|
||||
<PropertyGroup>
|
||||
<TargetName>$(Python38Output)</TargetName>
|
||||
<OutDir>$(ZoiteChatRel)plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;PYTHON_EXPORTS;Py_NO_LINK_LIB;$(OwnFlags);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<UndefinePreprocessorDefinitions>_DEBUG;Py_DEBUG;Py_REF_DEBUG;Py_TRACE_REFS;%(UndefinePreprocessorDefinitions)</UndefinePreprocessorDefinitions>
|
||||
<AdditionalOptions>/U_DEBUG /UPy_DEBUG /UPy_REF_DEBUG /UPy_TRACE_REFS %(AdditionalOptions)</AdditionalOptions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<AdditionalIncludeDirectories>$(Glib);$(Python38Path)\include;..\..\src\common;$(ZoiteChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<ModuleDefinitionFile>python.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>"$(Python38Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python38Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python38Path)\python.exe" generate_plugin.py ..\..\src\common\zoitechat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="generate_plugin.py" />
|
||||
<None Include="zoitechat.py" />
|
||||
<None Include="python.def" />
|
||||
<None Include="python.py" />
|
||||
<None Include="xchat.py" />
|
||||
<None Include="hexchat.py" />
|
||||
<None Include="_zoitechat.py" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
shared_module('winamp', 'winamp.c',
|
||||
dependencies: [libgio_dep, zoitechat_plugin_dep],
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'winamp.def',
|
||||
)
|
||||
@@ -1,153 +0,0 @@
|
||||
/********************* Winamp Plugin 0.3******************************
|
||||
*
|
||||
* Distribution: GPL
|
||||
*
|
||||
* Originally written by: Leo - leo.nard@free.fr
|
||||
* Modified by: SilvereX - SilvereX@karklas.mif.vu.lt
|
||||
* Modified again by: Derek Buitenhuis - daemon404@gmail.com
|
||||
* Modified yet again by: Berke Viktor - berkeviktor@aol.com
|
||||
*********************************************************************/
|
||||
|
||||
#include "windows.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <glib.h>
|
||||
|
||||
#include "zoitechat-plugin.h"
|
||||
|
||||
#define PLAYING 1
|
||||
#define PAUSED 3
|
||||
|
||||
static zoitechat_plugin *ph;
|
||||
|
||||
static int
|
||||
winamp(char *word[], char *word_eol[], void *userdata)
|
||||
{
|
||||
HWND hwndWinamp = FindWindowW(L"Winamp v1.x",NULL);
|
||||
|
||||
if (hwndWinamp)
|
||||
{
|
||||
if (!stricmp("PAUSE", word[2]))
|
||||
{
|
||||
if (SendMessage(hwndWinamp,WM_USER, 0, 104))
|
||||
{
|
||||
SendMessage(hwndWinamp, WM_COMMAND, 40046, 0);
|
||||
|
||||
if (SendMessage(hwndWinamp, WM_USER, 0, 104) == PLAYING)
|
||||
zoitechat_printf(ph, "Winamp: playing");
|
||||
else
|
||||
zoitechat_printf(ph, "Winamp: paused");
|
||||
}
|
||||
}
|
||||
else if (!stricmp("STOP", word[2]))
|
||||
{
|
||||
SendMessage(hwndWinamp, WM_COMMAND, 40047, 0);
|
||||
zoitechat_printf(ph, "Winamp: stopped");
|
||||
}
|
||||
else if (!stricmp("PLAY", word[2]))
|
||||
{
|
||||
SendMessage(hwndWinamp, WM_COMMAND, 40045, 0);
|
||||
zoitechat_printf(ph, "Winamp: playing");
|
||||
}
|
||||
else if (!stricmp("NEXT", word[2]))
|
||||
{
|
||||
SendMessage(hwndWinamp, WM_COMMAND, 40048, 0);
|
||||
zoitechat_printf(ph, "Winamp: next playlist entry");
|
||||
}
|
||||
else if (!stricmp("PREV", word[2]))
|
||||
{
|
||||
SendMessage(hwndWinamp, WM_COMMAND, 40044, 0);
|
||||
zoitechat_printf(ph, "Winamp: previous playlist entry");
|
||||
}
|
||||
else if (!stricmp("START", word[2]))
|
||||
{
|
||||
SendMessage(hwndWinamp, WM_COMMAND, 40154, 0);
|
||||
zoitechat_printf(ph, "Winamp: playlist start");
|
||||
}
|
||||
else if (!word_eol[2][0])
|
||||
{
|
||||
wchar_t wcurrent_play[2048];
|
||||
char *current_play, *p;
|
||||
int len = GetWindowTextW(hwndWinamp, wcurrent_play, G_N_ELEMENTS(wcurrent_play));
|
||||
|
||||
current_play = g_utf16_to_utf8 (wcurrent_play, len, NULL, NULL, NULL);
|
||||
if (!current_play)
|
||||
{
|
||||
zoitechat_print (ph, "Winamp: Error getting song information.");
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
if (strchr(current_play, '-'))
|
||||
{
|
||||
/* Remove any trailing text and whitespace */
|
||||
p = current_play + strlen(current_play) - 8;
|
||||
while (p >= current_play)
|
||||
{
|
||||
if (!strnicmp(p, "- Winamp", 8))
|
||||
break;
|
||||
p--;
|
||||
}
|
||||
|
||||
if (p >= current_play)
|
||||
p--;
|
||||
|
||||
while (p >= current_play && *p == ' ')
|
||||
p--;
|
||||
*++p = '\0';
|
||||
|
||||
/* Ignore any leading track number */
|
||||
p = strstr (current_play, ". ");
|
||||
if (p)
|
||||
p += 2;
|
||||
else
|
||||
p = current_play;
|
||||
|
||||
if (*p != '\0')
|
||||
zoitechat_commandf (ph, "me is now playing: %s", p);
|
||||
else
|
||||
zoitechat_print (ph, "Winamp: No song information found.");
|
||||
g_free (current_play);
|
||||
}
|
||||
else
|
||||
zoitechat_print(ph, "Winamp: Nothing being played.");
|
||||
}
|
||||
else
|
||||
zoitechat_printf(ph, "Usage: /WINAMP [PAUSE|PLAY|STOP|NEXT|PREV|START]\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
zoitechat_print(ph, "Winamp not found.\n");
|
||||
}
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
int
|
||||
zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
|
||||
char **plugin_name,
|
||||
char **plugin_desc,
|
||||
char **plugin_version,
|
||||
char *arg)
|
||||
{
|
||||
ph = plugin_handle;
|
||||
|
||||
*plugin_name = "Winamp";
|
||||
*plugin_desc = "Winamp plugin for ZoiteChat";
|
||||
*plugin_version = "0.6";
|
||||
|
||||
zoitechat_hook_command (ph, "WINAMP", ZOITECHAT_PRI_NORM, winamp, "Usage: /WINAMP [PAUSE|PLAY|STOP|NEXT|PREV|START] - control Winamp or show what's currently playing", 0);
|
||||
zoitechat_command (ph, "MENU -ishare\\music.png ADD \"Window/Display Current Song (Winamp)\" \"WINAMP\"");
|
||||
|
||||
zoitechat_print (ph, "Winamp plugin loaded\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
zoitechat_plugin_deinit(void)
|
||||
{
|
||||
zoitechat_command (ph, "MENU DEL \"Window/Display Current Song (Winamp)\"");
|
||||
zoitechat_print (ph, "Winamp plugin unloaded\n");
|
||||
return 1;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
EXPORTS
|
||||
zoitechat_plugin_init
|
||||
zoitechat_plugin_deinit
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{E78C0D9A-798E-4BF6-B0CC-6FECB8CA2FCE}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>winamp</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="..\..\win32\zoitechat.props" />
|
||||
<PropertyGroup>
|
||||
<TargetName>hcwinamp</TargetName>
|
||||
<OutDir>$(ZoiteChatRel)plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;WINAMP_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(OpenSslInclude);$(Glib);..\..\src\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<ModuleDefinitionFile>winamp.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="winamp.def" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="winamp.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="winamp.def">
|
||||
<Filter>Resource Files</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="winamp.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -4,6 +4,7 @@ data/misc/net.zoite.Zoitechat.ThemeManager.desktop.in
|
||||
src/common/cfgfiles.c
|
||||
src/common/chanopt.c
|
||||
src/common/dcc.c
|
||||
src/common/emoji-data.c
|
||||
src/common/zoitechat.c
|
||||
src/common/ignore.c
|
||||
src/common/inbound.c
|
||||
@@ -24,6 +25,7 @@ src/fe-gtk/banlist.c
|
||||
src/fe-gtk/chanlist.c
|
||||
src/fe-gtk/dccgui.c
|
||||
src/fe-gtk/editlist.c
|
||||
src/fe-gtk/emoji-picker.c
|
||||
src/fe-gtk/fe-gtk.c
|
||||
src/fe-gtk/fkeys.c
|
||||
src/fe-gtk/gtkutil.c
|
||||
|
||||
17
readme.md
17
readme.md
@@ -31,12 +31,19 @@
|
||||
<br />
|
||||
|
||||
|
||||
ZoiteChat is an HexChat based IRC client for Windows and UNIX-like operating systems.
|
||||
See [IRCHelp.org](http://irchelp.org) for information about IRC in general.
|
||||
For more information on ZoiteChat please read our [documentation](https://docs.zoitechat.org/):
|
||||
- [Downloads](https://zoitechat.org/download)
|
||||
ZoiteChat is a GTK3 IRC client based on HexChat, available for Windows and UNIX-like operating systems.
|
||||
|
||||
- [Troubleshooting](troubleshooting.md)
|
||||
Features include HexChat-compatible Python, Perl and Lua scripting support, a plugin API,
|
||||
multiple server/channel windows, spell checking, multiple authentication methods including SASL,
|
||||
and customizable notifications.
|
||||
|
||||
See [IRCHelp.org](http://irchelp.org) for information about IRC in general.
|
||||
|
||||
For more information on ZoiteChat:
|
||||
|
||||
- [Main Documentation](https://docs.zoitechat.org/) and [FAQ](https://docs.zoitechat.org/en/latest/faq.html)
|
||||
|
||||
- [Downloads](https://zoitechat.org/download.php)
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -40,18 +40,18 @@
|
||||
|
||||
#define DEF_FONT "Monospace 9"
|
||||
#ifdef WIN32
|
||||
#define DEF_FONT_ALTER "Segoe UI Emoji,Arial Unicode MS,Lucida Sans Unicode,Meiryo,Symbola,Unifont"
|
||||
#define DEF_FONT_ALTER "Segoe UI Emoji,Segoe UI Symbol,Arial Unicode MS,Lucida Sans Unicode,Meiryo,Symbola,Unifont"
|
||||
#else
|
||||
#define DEF_FONT_ALTER "Arial Unicode MS,Segoe UI Emoji,Lucida Sans Unicode,Meiryo,Symbola,Unifont"
|
||||
#endif
|
||||
|
||||
const char * const languages[LANGUAGES_LENGTH] = {
|
||||
"af", "sq", "am", "ast", "az", "eu", "be", "bg", "ca", "zh_CN", /* 0 .. 9 */
|
||||
"zh_TW", "cs", "da", "nl", "en_GB", "en", "et", "fi", "fr", "gl", /* 10 .. 19 */
|
||||
"de", "el", "gu", "hi", "hu", "id", "it", "ja_JP", "kn", "rw", /* 20 .. 29 */
|
||||
"ko", "lv", "lt", "mk", "ml", "ms", "nb", "no", "pl", "pt", /* 30 .. 39 */
|
||||
"pt_BR", "pa", "ru", "sr", "sk", "sl", "es", "sv", "th", "tr", /* 40 .. 49 */
|
||||
"uk", "vi", "wa" /* 50 .. */
|
||||
"af", "sq", "am", "ast", "az", "eu", "be", "bg", "ca", "zh_CN",
|
||||
"zh_TW", "cs", "da", "nl", "en_GB", "en", "et", "fi", "fr", "gl",
|
||||
"de", "el", "gu", "hi", "hu", "id", "it", "ja_JP", "kn", "rw",
|
||||
"ko", "lv", "lt", "mk", "ml", "ms", "nb", "no", "pl", "pt",
|
||||
"pt_BR", "pa", "ru", "sr", "sk", "sl", "es", "sv", "th", "tr",
|
||||
"uk", "vi", "wa"
|
||||
};
|
||||
|
||||
void
|
||||
@@ -78,8 +78,6 @@ list_addentry (GSList ** list, char *cmd, char *name)
|
||||
*list = g_slist_append (*list, pop);
|
||||
}
|
||||
|
||||
/* read it in from a buffer to our linked list */
|
||||
|
||||
static void
|
||||
list_load_from_data (GSList ** list, char *ibuf, int size)
|
||||
{
|
||||
@@ -181,9 +179,9 @@ list_delentry (GSList ** list, char *name)
|
||||
char *
|
||||
cfg_get_str (char *cfg, const char *var, char *dest, int dest_len)
|
||||
{
|
||||
char buffer[128]; /* should be plenty for a variable name */
|
||||
char buffer[128];
|
||||
|
||||
sprintf (buffer, "%s ", var); /* add one space, this way it works against var - var2 checks too */
|
||||
sprintf (buffer, "%s ", var);
|
||||
|
||||
while (1)
|
||||
{
|
||||
@@ -197,8 +195,6 @@ cfg_get_str (char *cfg, const char *var, char *dest, int dest_len)
|
||||
cfg++;
|
||||
while (*cfg == ' ')
|
||||
cfg++;
|
||||
/*while (*cfg == ' ' || *cfg == '=')
|
||||
cfg++; */
|
||||
value = cfg;
|
||||
while (*cfg != 0 && *cfg != '\n')
|
||||
cfg++;
|
||||
@@ -293,7 +289,7 @@ cfg_get_int (char *cfg, char *var)
|
||||
return atoi (str);
|
||||
}
|
||||
|
||||
char *xdir = NULL; /* utf-8 encoding */
|
||||
char *xdir = NULL;
|
||||
|
||||
#ifdef WIN32
|
||||
#include <windows.h>
|
||||
@@ -416,6 +412,7 @@ const struct prefs vars[] =
|
||||
{"gui_dialog_left", P_OFFINT (hex_gui_dialog_left), TYPE_INT},
|
||||
{"gui_dialog_top", P_OFFINT (hex_gui_dialog_top), TYPE_INT},
|
||||
{"gui_dialog_width", P_OFFINT (hex_gui_dialog_width), TYPE_INT},
|
||||
{"gui_emoji_skin_tone", P_OFFINT (hex_gui_emoji_skin_tone), TYPE_INT},
|
||||
{"gui_filesize_iec", P_OFFINT (hex_gui_filesize_iec), TYPE_BOOL},
|
||||
{"gui_focus_omitalerts", P_OFFINT (hex_gui_focus_omitalerts), TYPE_BOOL},
|
||||
{"gui_hide_menu", P_OFFINT (hex_gui_hide_menu), TYPE_BOOL},
|
||||
@@ -428,13 +425,13 @@ const struct prefs vars[] =
|
||||
{"gui_lagometer", P_OFFINT (hex_gui_lagometer), TYPE_INT},
|
||||
{"gui_lang", P_OFFINT (hex_gui_lang), TYPE_INT},
|
||||
{"gui_mode_buttons", P_OFFINT (hex_gui_mode_buttons), TYPE_BOOL},
|
||||
{"gui_mode_buttons_inline", P_OFFINT (hex_gui_mode_buttons_inline), TYPE_BOOL},
|
||||
{"gui_pane_divider_position", P_OFFINT (hex_gui_pane_divider_position), TYPE_INT},
|
||||
{"gui_pane_left_size", P_OFFINT (hex_gui_pane_left_size), TYPE_INT},
|
||||
{"gui_pane_right_size", P_OFFINT (hex_gui_pane_right_size), TYPE_INT},
|
||||
{"gui_pane_right_size_min", P_OFFINT (hex_gui_pane_right_size_min), TYPE_INT},
|
||||
{"gui_quit_dialog", P_OFFINT (hex_gui_quit_dialog), TYPE_BOOL},
|
||||
{"gui_search_pos", P_OFFINT (hex_gui_search_pos), TYPE_INT},
|
||||
/* {"gui_single", P_OFFINT (hex_gui_single), TYPE_BOOL}, */
|
||||
{"gui_slist_fav", P_OFFINT (hex_gui_slist_fav), TYPE_BOOL},
|
||||
{"gui_slist_select", P_OFFINT (hex_gui_slist_select), TYPE_INT},
|
||||
{"gui_slist_skip", P_OFFINT (hex_gui_slist_skip), TYPE_BOOL},
|
||||
@@ -447,6 +444,7 @@ const struct prefs vars[] =
|
||||
{"gui_tab_layout", P_OFFINT (hex_gui_tab_layout), TYPE_INT},
|
||||
{"gui_tab_closebuttons", P_OFFINT (hex_gui_tab_closebuttons), TYPE_BOOL},
|
||||
{"gui_tab_middleclose", P_OFFINT (hex_gui_tab_middleclose), TYPE_BOOL},
|
||||
{"gui_mouse_scroll_speed", P_OFFINT (hex_gui_mouse_scroll_speed), TYPE_INT},
|
||||
{"gui_tab_newtofront", P_OFFINT (hex_gui_tab_newtofront), TYPE_INT},
|
||||
{"gui_tab_pos", P_OFFINT (hex_gui_tab_pos), TYPE_INT},
|
||||
{"gui_tab_scrollchans", P_OFFINT (hex_gui_tab_scrollchans), TYPE_BOOL},
|
||||
@@ -457,6 +455,7 @@ const struct prefs vars[] =
|
||||
{"gui_tab_utils", P_OFFINT (hex_gui_tab_utils), TYPE_BOOL},
|
||||
{"gui_throttlemeter", P_OFFINT (hex_gui_throttlemeter), TYPE_INT},
|
||||
{"gui_topicbar", P_OFFINT (hex_gui_topicbar), TYPE_BOOL},
|
||||
{"gui_topicbar_multiline", P_OFFINT (hex_gui_topicbar_multiline), TYPE_BOOL},
|
||||
{"gui_transparency", P_OFFINT (hex_gui_transparency), TYPE_INT},
|
||||
{"gui_tray", P_OFFINT (hex_gui_tray), TYPE_BOOL},
|
||||
{"gui_tray_away", P_OFFINT (hex_gui_tray_away), TYPE_BOOL},
|
||||
@@ -517,6 +516,7 @@ const struct prefs vars[] =
|
||||
{"irc_conf_mode", P_OFFINT (hex_irc_conf_mode), TYPE_BOOL},
|
||||
{"irc_extra_hilight", P_OFFSET (hex_irc_extra_hilight), TYPE_STR},
|
||||
{"irc_hide_nickchange", P_OFFINT (hex_irc_hide_nickchange), TYPE_BOOL},
|
||||
{"irc_hide_join_part_hostmask", P_OFFINT (hex_irc_hide_join_part_hostmask), TYPE_BOOL},
|
||||
{"irc_hide_version", P_OFFINT (hex_irc_hide_version), TYPE_BOOL},
|
||||
{"irc_hidehost", P_OFFINT (hex_irc_hidehost), TYPE_BOOL},
|
||||
{"irc_id_ntext", P_OFFSET (hex_irc_id_ntext), TYPE_STR},
|
||||
@@ -548,6 +548,10 @@ const struct prefs vars[] =
|
||||
#endif
|
||||
{"net_bind_host", P_OFFSET (hex_net_bind_host), TYPE_STR},
|
||||
{"net_ping_timeout", P_OFFINT (hex_net_ping_timeout), TYPE_INT, zoitechat_reinit_timers},
|
||||
{"net_lag_check", P_OFFINT (hex_net_lag_check), TYPE_INT, zoitechat_reinit_timers},
|
||||
{"net_keepalive_idle", P_OFFINT (hex_net_keepalive_idle), TYPE_INT},
|
||||
{"net_keepalive_interval", P_OFFINT (hex_net_keepalive_interval), TYPE_INT},
|
||||
{"net_keepalive_count", P_OFFINT (hex_net_keepalive_count), TYPE_INT},
|
||||
{"net_proxy_auth", P_OFFINT (hex_net_proxy_auth), TYPE_BOOL},
|
||||
{"net_proxy_host", P_OFFSET (hex_net_proxy_host), TYPE_STR},
|
||||
{"net_proxy_pass", P_OFFSET (hex_net_proxy_pass), TYPE_STR},
|
||||
@@ -782,7 +786,10 @@ load_default_config(void)
|
||||
prefs.hex_gui_tab_middleclose = 1;
|
||||
prefs.hex_gui_tab_server = 1;
|
||||
prefs.hex_gui_tab_sort = 1;
|
||||
prefs.hex_gui_tab_scrollchans = 1;
|
||||
prefs.hex_gui_mouse_scroll_speed = 10;
|
||||
prefs.hex_gui_topicbar = 1;
|
||||
prefs.hex_gui_topicbar_multiline = 1;
|
||||
prefs.hex_gui_transparency = 255;
|
||||
prefs.hex_gui_tray = 1;
|
||||
prefs.hex_gui_tray_blink = 1;
|
||||
@@ -857,6 +864,10 @@ load_default_config(void)
|
||||
prefs.hex_irc_ban_type = 1;
|
||||
prefs.hex_irc_join_delay = 5;
|
||||
prefs.hex_net_ping_timeout = 60;
|
||||
prefs.hex_net_lag_check = 60;
|
||||
prefs.hex_net_keepalive_idle = 60;
|
||||
prefs.hex_net_keepalive_interval = 20;
|
||||
prefs.hex_net_keepalive_count = 3;
|
||||
prefs.hex_net_reconnect_delay = 10;
|
||||
prefs.hex_notify_timeout = 15;
|
||||
prefs.hex_text_max_indent = 256;
|
||||
@@ -1167,8 +1178,11 @@ set_showval (session *sess, const struct prefs *var, char *tbuf)
|
||||
switch (var->type)
|
||||
{
|
||||
case TYPE_STR:
|
||||
sprintf (tbuf + len, "\0033:\017 %s\n", (char *) &prefs + var->offset);
|
||||
break;
|
||||
{
|
||||
const char *value = (char *) &prefs + var->offset;
|
||||
sprintf (tbuf + len, "\0033:\017 %s\n", value ? value : "");
|
||||
}
|
||||
break;
|
||||
case TYPE_INT:
|
||||
sprintf (tbuf + len, "\0033:\017 %d\n", *((int *) &prefs + var->offset));
|
||||
break;
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
<ClInclude Include="plugin-timer.h" />
|
||||
<ClInclude Include="plugin.h" />
|
||||
<ClInclude Include="proto-irc.h" />
|
||||
<ClInclude Include="public_suffix_data.h" />
|
||||
<ClInclude Include="server.h" />
|
||||
<ClInclude Include="servlist.h" />
|
||||
<ClInclude Include="secretstore.h" />
|
||||
<ClInclude Include="ssl.h" />
|
||||
<ClInclude Include="scram.h" />
|
||||
<ClInclude Include="sysinfo\sysinfo.h" />
|
||||
@@ -53,6 +55,7 @@
|
||||
<ClCompile Include="chanopt.c" />
|
||||
<ClCompile Include="ctcp.c" />
|
||||
<ClCompile Include="dcc.c" />
|
||||
<ClCompile Include="emoji-data.c" />
|
||||
<ClCompile Include="history.c" />
|
||||
<ClCompile Include="plugin-identd.c" />
|
||||
<ClCompile Include="ignore.c" />
|
||||
@@ -67,6 +70,7 @@
|
||||
<ClCompile Include="proto-irc.c" />
|
||||
<ClCompile Include="server.c" />
|
||||
<ClCompile Include="servlist.c" />
|
||||
<ClCompile Include="secretstore.c" />
|
||||
<ClCompile Include="ssl.c" />
|
||||
<ClCompile Include="scram.c" />
|
||||
<ClCompile Include="sts.c" />
|
||||
@@ -109,6 +113,8 @@
|
||||
<Command><![CDATA[
|
||||
SET SOLUTIONDIR=$(SolutionDir)..\
|
||||
"$(Python3Path)\python.exe" $(ProjectDir)make-te.py "$(ProjectDir)textevents.in" "$(ZoiteChatLib)textevents.h" "$(ZoiteChatLib)textenums.h"
|
||||
"$(Python3Path)\python.exe" $(ProjectDir)gen-public-suffix.py "$(ZoiteChatLib)public_suffix_data.h"
|
||||
"$(Python3Path)\python.exe" "$(SolutionDir)..\tools\gen-emoji-data.py" "$(SolutionDir)..\data\emoji\emoji-test.txt" "$(SolutionDir)..\data\emoji\cldr-annotations-en.xml" "$(SolutionDir)..\data\emoji\cldr-annotations-derived-en.xml" "$(SolutionDir)..\data\emoji\gemoji.json" "$(SolutionDir)..\data\emoji\VERSIONS" "$(ZoiteChatLib)emoji-data-table.h"
|
||||
powershell -File "$(SolutionDir)..\win32\version-template.ps1" "$(SolutionDir)..\win32\config.h.tt" "$(ZoiteChatLib)config.h"
|
||||
$(GlibGenMarshal) --prefix=_zoitechat_marshal --header "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.h"
|
||||
$(GlibGenMarshal) --prefix=_zoitechat_marshal --body "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.c"
|
||||
|
||||
@@ -65,12 +65,18 @@
|
||||
<ClInclude Include="proto-irc.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="public_suffix_data.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="server.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="servlist.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="secretstore.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ssl.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@@ -175,6 +181,9 @@
|
||||
<ClCompile Include="servlist.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="secretstore.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ssl.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -82,10 +82,6 @@ has_theme_argument (void)
|
||||
|
||||
void
|
||||
zoitechat_remote (void)
|
||||
/* TODO: dbus_g_connection_unref (connection) are commented because it makes
|
||||
* dbus to crash. Fixed in dbus >=0.70 ?!?
|
||||
* https://launchpad.net/distros/ubuntu/+source/dbus/+bug/54375
|
||||
*/
|
||||
{
|
||||
GDBusConnection *connection;
|
||||
GDBusProxy *dbus = NULL;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,203 +1,122 @@
|
||||
/* example.c - program to demonstrate some D-BUS stuffs.
|
||||
* Copyright (C) 2006 Claessens Xavier
|
||||
*
|
||||
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*
|
||||
* Claessens Xavier
|
||||
* xclaesse@gmail.com
|
||||
*/
|
||||
|
||||
#include <config.h>
|
||||
#include <dbus/dbus-glib.h>
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
#include <stdlib.h>
|
||||
#include "../marshal.c"
|
||||
|
||||
#define DBUS_SERVICE "org.zoitechat.service"
|
||||
#define DBUS_REMOTE "/org/zoitechat/Remote"
|
||||
#define DBUS_REMOTE_CONNECTION_INTERFACE "org.zoitechat.connection"
|
||||
#define DBUS_REMOTE_PLUGIN_INTERFACE "org.zoitechat.plugin"
|
||||
#define DBUS_CONNECTION_PATH "/org/zoitechat"
|
||||
#define DBUS_CONNECTION_INTERFACE "org.zoitechat.connection"
|
||||
#define DBUS_PLUGIN_INTERFACE "org.zoitechat.plugin"
|
||||
|
||||
guint command_id;
|
||||
guint server_id;
|
||||
|
||||
static void
|
||||
write_error (const char *message,
|
||||
GError **error)
|
||||
{
|
||||
if (error == NULL || *error == NULL) {
|
||||
return;
|
||||
}
|
||||
g_printerr ("%s: %s\n", message, (*error)->message);
|
||||
g_clear_error (error);
|
||||
}
|
||||
|
||||
static void
|
||||
test_server_cb (DBusGProxy *proxy,
|
||||
char *word[],
|
||||
char *word_eol[],
|
||||
guint hook_id,
|
||||
guint context_id,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (hook_id == server_id) {
|
||||
g_print ("message: %s\n", word_eol[0]);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
test_command_cb (DBusGProxy *proxy,
|
||||
char *word[],
|
||||
char *word_eol[],
|
||||
guint hook_id,
|
||||
guint context_id,
|
||||
gpointer user_data)
|
||||
static gboolean
|
||||
call_sync (GDBusProxy *proxy, const char *method, GVariant *params, GVariant **out)
|
||||
{
|
||||
GError *error = NULL;
|
||||
GVariant *result;
|
||||
|
||||
if (hook_id == command_id) {
|
||||
if (!dbus_g_proxy_call (proxy, "Unhook",
|
||||
&error,
|
||||
G_TYPE_UINT, hook_id,
|
||||
G_TYPE_INVALID, G_TYPE_INVALID)) {
|
||||
write_error ("Failed to complete unhook", &error);
|
||||
}
|
||||
/* Now if you write "/test blah" again in the ZoiteChat window
|
||||
* you'll get a "Unknown command" error message */
|
||||
g_print ("test command received: %s\n", word_eol[1]);
|
||||
if (!dbus_g_proxy_call (proxy, "Print",
|
||||
&error,
|
||||
G_TYPE_STRING, "test command succeed",
|
||||
G_TYPE_INVALID,
|
||||
G_TYPE_INVALID)) {
|
||||
write_error ("Failed to complete Print", &error);
|
||||
}
|
||||
result = g_dbus_proxy_call_sync (proxy,
|
||||
method,
|
||||
params,
|
||||
G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
NULL,
|
||||
&error);
|
||||
if (!result)
|
||||
{
|
||||
g_printerr ("%s failed: %s\n", method, error->message);
|
||||
g_clear_error (&error);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
unload_cb (void)
|
||||
{
|
||||
g_print ("Good bye !\n");
|
||||
exit (EXIT_SUCCESS);
|
||||
if (out)
|
||||
*out = result;
|
||||
else
|
||||
g_variant_unref (result);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
DBusGConnection *connection;
|
||||
DBusGProxy *remote_object;
|
||||
GMainLoop *mainloop;
|
||||
gchar *path;
|
||||
GDBusConnection *connection;
|
||||
GDBusProxy *connection_proxy;
|
||||
GDBusProxy *plugin_proxy;
|
||||
GVariant *connect_result = NULL;
|
||||
gchar *remote_path = NULL;
|
||||
gchar *command = NULL;
|
||||
const char *path_tmp;
|
||||
int status = EXIT_FAILURE;
|
||||
GError *error = NULL;
|
||||
|
||||
#if ! GLIB_CHECK_VERSION (2, 36, 0)
|
||||
g_type_init ();
|
||||
#endif
|
||||
|
||||
connection = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
|
||||
if (connection == NULL) {
|
||||
write_error ("Couldn't connect to session bus", &error);
|
||||
connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
|
||||
if (!connection)
|
||||
{
|
||||
g_printerr ("Bus connection failed: %s\n", error->message);
|
||||
g_clear_error (&error);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
remote_object = dbus_g_proxy_new_for_name (connection,
|
||||
DBUS_SERVICE,
|
||||
DBUS_REMOTE,
|
||||
DBUS_REMOTE_CONNECTION_INTERFACE);
|
||||
if (!dbus_g_proxy_call (remote_object, "Connect",
|
||||
&error,
|
||||
G_TYPE_STRING, argv[0],
|
||||
G_TYPE_STRING, "example",
|
||||
G_TYPE_STRING, "Example of a D-Bus client",
|
||||
G_TYPE_STRING, "1.0",
|
||||
G_TYPE_INVALID,
|
||||
G_TYPE_STRING, &path, G_TYPE_INVALID)) {
|
||||
write_error ("Failed to complete Connect", &error);
|
||||
|
||||
connection_proxy = g_dbus_proxy_new_sync (connection,
|
||||
G_DBUS_PROXY_FLAGS_NONE,
|
||||
NULL,
|
||||
DBUS_SERVICE,
|
||||
DBUS_CONNECTION_PATH,
|
||||
DBUS_CONNECTION_INTERFACE,
|
||||
NULL,
|
||||
&error);
|
||||
if (!connection_proxy)
|
||||
{
|
||||
g_printerr ("Connection proxy failed: %s\n", error->message);
|
||||
g_clear_error (&error);
|
||||
g_object_unref (connection);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
g_object_unref (remote_object);
|
||||
|
||||
remote_object = dbus_g_proxy_new_for_name (connection,
|
||||
DBUS_SERVICE,
|
||||
path,
|
||||
DBUS_REMOTE_PLUGIN_INTERFACE);
|
||||
g_free (path);
|
||||
if (!call_sync (connection_proxy,
|
||||
"Connect",
|
||||
g_variant_new ("(ssss)", "example", "example", "GDBus example", "1.0"),
|
||||
&connect_result))
|
||||
goto cleanup;
|
||||
|
||||
if (!dbus_g_proxy_call (remote_object, "HookCommand",
|
||||
&error,
|
||||
G_TYPE_STRING, "test",
|
||||
G_TYPE_INT, 0,
|
||||
G_TYPE_STRING, "Simple D-BUS example",
|
||||
G_TYPE_INT, 1, G_TYPE_INVALID,
|
||||
G_TYPE_UINT, &command_id, G_TYPE_INVALID)) {
|
||||
write_error ("Failed to complete HookCommand", &error);
|
||||
return EXIT_FAILURE;
|
||||
g_variant_get (connect_result, "(&s)", &path_tmp);
|
||||
remote_path = g_strdup (path_tmp);
|
||||
g_variant_unref (connect_result);
|
||||
connect_result = NULL;
|
||||
|
||||
plugin_proxy = g_dbus_proxy_new_sync (connection,
|
||||
G_DBUS_PROXY_FLAGS_NONE,
|
||||
NULL,
|
||||
DBUS_SERVICE,
|
||||
remote_path,
|
||||
DBUS_PLUGIN_INTERFACE,
|
||||
NULL,
|
||||
&error);
|
||||
if (!plugin_proxy)
|
||||
{
|
||||
g_printerr ("Plugin proxy failed: %s\n", error->message);
|
||||
g_clear_error (&error);
|
||||
goto cleanup;
|
||||
}
|
||||
g_print ("Command hook id=%d\n", command_id);
|
||||
|
||||
if (!dbus_g_proxy_call (remote_object, "HookServer",
|
||||
&error,
|
||||
G_TYPE_STRING, "RAW LINE",
|
||||
G_TYPE_INT, 0,
|
||||
G_TYPE_INT, 0, G_TYPE_INVALID,
|
||||
G_TYPE_UINT, &server_id, G_TYPE_INVALID)) {
|
||||
write_error ("Failed to complete HookServer", &error);
|
||||
return EXIT_FAILURE;
|
||||
if (argc > 1)
|
||||
command = g_strjoinv (" ", &argv[1]);
|
||||
else
|
||||
command = g_strdup ("gui focus");
|
||||
|
||||
if (!call_sync (plugin_proxy, "Command", g_variant_new ("(s)", command), NULL))
|
||||
{
|
||||
g_object_unref (plugin_proxy);
|
||||
goto cleanup;
|
||||
}
|
||||
g_print ("Server hook id=%d\n", server_id);
|
||||
|
||||
dbus_g_object_register_marshaller (
|
||||
_zoitechat_marshal_VOID__POINTER_POINTER_UINT_UINT,
|
||||
G_TYPE_NONE,
|
||||
G_TYPE_STRV, G_TYPE_STRV, G_TYPE_UINT, G_TYPE_UINT,
|
||||
G_TYPE_INVALID);
|
||||
call_sync (connection_proxy, "Disconnect", g_variant_new ("()"), NULL);
|
||||
status = EXIT_SUCCESS;
|
||||
g_object_unref (plugin_proxy);
|
||||
|
||||
dbus_g_object_register_marshaller (
|
||||
g_cclosure_marshal_VOID__VOID,
|
||||
G_TYPE_NONE,
|
||||
G_TYPE_INVALID);
|
||||
|
||||
dbus_g_proxy_add_signal (remote_object, "CommandSignal",
|
||||
G_TYPE_STRV,
|
||||
G_TYPE_STRV,
|
||||
G_TYPE_UINT,
|
||||
G_TYPE_UINT,
|
||||
G_TYPE_INVALID);
|
||||
dbus_g_proxy_connect_signal (remote_object, "CommandSignal",
|
||||
G_CALLBACK (test_command_cb),
|
||||
NULL, NULL);
|
||||
|
||||
dbus_g_proxy_add_signal (remote_object, "ServerSignal",
|
||||
G_TYPE_STRV,
|
||||
G_TYPE_STRV,
|
||||
G_TYPE_UINT,
|
||||
G_TYPE_UINT,
|
||||
G_TYPE_INVALID);
|
||||
dbus_g_proxy_connect_signal (remote_object, "ServerSignal",
|
||||
G_CALLBACK (test_server_cb),
|
||||
NULL, NULL);
|
||||
|
||||
dbus_g_proxy_add_signal (remote_object, "UnloadSignal",
|
||||
G_TYPE_INVALID);
|
||||
dbus_g_proxy_connect_signal (remote_object, "UnloadSignal",
|
||||
G_CALLBACK (unload_cb),
|
||||
NULL, NULL);
|
||||
|
||||
/* Now you can write on the ZoiteChat windows: "/test arg1 arg2 ..." */
|
||||
mainloop = g_main_loop_new (NULL, FALSE);
|
||||
g_main_loop_run (mainloop);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
cleanup:
|
||||
g_free (command);
|
||||
g_free (remote_path);
|
||||
g_object_unref (connection_proxy);
|
||||
g_object_unref (connection);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
dbus_deps = [
|
||||
dbus_glib_dep
|
||||
]
|
||||
dbus_deps = []
|
||||
|
||||
dbus_sources = [
|
||||
'dbus-plugin.c',
|
||||
@@ -35,16 +33,8 @@ configure_file(
|
||||
install_dir: dbus_service_dir
|
||||
)
|
||||
|
||||
dbus_binding_tool = find_program('dbus-binding-tool')
|
||||
dbus_remote_object = custom_target('remote-object-glue',
|
||||
input: 'remote-object.xml',
|
||||
output: 'remote-object-glue.h',
|
||||
command: [dbus_binding_tool, '--prefix=remote_object', '--mode=glib-server',
|
||||
'--output=@OUTPUT@', '@INPUT@']
|
||||
)
|
||||
|
||||
zoitechat_dbus = static_library('zoitechatdbus',
|
||||
sources: [dbus_remote_object, marshal] + dbus_sources,
|
||||
sources: marshal + dbus_sources,
|
||||
c_args: dbus_cargs,
|
||||
dependencies: common_deps + dbus_deps,
|
||||
include_directories: dbus_includes,
|
||||
|
||||
@@ -304,29 +304,24 @@ dcc_check_timeouts (void)
|
||||
static int
|
||||
dcc_lookup_proxy (char *host, struct sockaddr_in *addr)
|
||||
{
|
||||
struct hostent *h;
|
||||
static char *cache_host = NULL;
|
||||
static guint32 cache_addr;
|
||||
|
||||
/* too lazy to thread this, so we cache results */
|
||||
if (cache_host)
|
||||
{
|
||||
if (strcmp (host, cache_host) == 0)
|
||||
{
|
||||
memcpy (&addr->sin_addr, &cache_addr, 4);
|
||||
addr->sin_addr.s_addr = cache_addr;
|
||||
return TRUE;
|
||||
}
|
||||
g_free (cache_host);
|
||||
cache_host = NULL;
|
||||
}
|
||||
|
||||
h = gethostbyname (host);
|
||||
if (h != NULL && h->h_length == 4 && h->h_addr_list[0] != NULL)
|
||||
if (net_lookup_ipv4 (host, &addr->sin_addr.s_addr))
|
||||
{
|
||||
memcpy (&addr->sin_addr, h->h_addr_list[0], 4);
|
||||
memcpy (&cache_addr, h->h_addr_list[0], 4);
|
||||
cache_addr = addr->sin_addr.s_addr;
|
||||
cache_host = g_strdup (host);
|
||||
/* cppcheck-suppress memleak */
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -1614,25 +1609,14 @@ dcc_accept (GIOChannel *source, GIOCondition condition, struct DCC *dcc)
|
||||
}
|
||||
|
||||
guint32
|
||||
dcc_get_my_address (session *sess) /* the address we'll tell the other person */
|
||||
dcc_get_my_address (session *sess)
|
||||
{
|
||||
struct hostent *dns_query;
|
||||
guint32 addr = 0;
|
||||
|
||||
if (prefs.hex_dcc_ip_from_server && sess->server->dcc_ip)
|
||||
addr = sess->server->dcc_ip;
|
||||
else if (prefs.hex_dcc_ip[0])
|
||||
{
|
||||
dns_query = gethostbyname ((const char *) prefs.hex_dcc_ip);
|
||||
|
||||
if (dns_query != NULL &&
|
||||
dns_query->h_length == 4 &&
|
||||
dns_query->h_addr_list[0] != NULL)
|
||||
{
|
||||
/*we're offered at least one IPv4 address: we take the first*/
|
||||
addr = *((guint32*) dns_query->h_addr_list[0]);
|
||||
}
|
||||
}
|
||||
net_lookup_ipv4 ((const char *) prefs.hex_dcc_ip, &addr);
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
239
src/common/emoji-data.c
Normal file
239
src/common/emoji-data.c
Normal file
@@ -0,0 +1,239 @@
|
||||
/* ZoiteChat
|
||||
* Copyright (C) 2026 deepend-tildeclub.
|
||||
*
|
||||
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
#include "emoji-data.h"
|
||||
|
||||
#include "emoji-data-table.h"
|
||||
|
||||
gsize
|
||||
emoji_data_count (void)
|
||||
{
|
||||
return G_N_ELEMENTS (emoji_entries_table);
|
||||
}
|
||||
|
||||
const EmojiEntry *
|
||||
emoji_data_entry (gsize index)
|
||||
{
|
||||
g_return_val_if_fail (index < G_N_ELEMENTS (emoji_entries_table), NULL);
|
||||
|
||||
return &emoji_entries_table[index];
|
||||
}
|
||||
|
||||
static GHashTable *
|
||||
emoji_sequence_index (void)
|
||||
{
|
||||
static GHashTable *table = NULL;
|
||||
gsize i;
|
||||
int tone;
|
||||
|
||||
if (table)
|
||||
return table;
|
||||
|
||||
table = g_hash_table_new (g_str_hash, g_str_equal);
|
||||
for (i = 0; i < G_N_ELEMENTS (emoji_entries_table); i++)
|
||||
{
|
||||
const EmojiEntry *entry = &emoji_entries_table[i];
|
||||
|
||||
g_hash_table_insert (table, (gpointer) entry->sequence,
|
||||
GSIZE_TO_POINTER (i * EMOJI_TONE_COUNT + 1));
|
||||
if (entry->tone_set < 0)
|
||||
continue;
|
||||
for (tone = EMOJI_TONE_LIGHT; tone < EMOJI_TONE_COUNT; tone++)
|
||||
{
|
||||
const char *seq =
|
||||
emoji_tone_sets_table[entry->tone_set].sequence[tone - 1];
|
||||
|
||||
g_hash_table_insert (table, (gpointer) seq,
|
||||
GSIZE_TO_POINTER (i * EMOJI_TONE_COUNT + tone + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
const EmojiEntry *
|
||||
emoji_data_find_by_sequence (const char *sequence, EmojiTone *tone_out)
|
||||
{
|
||||
gpointer value;
|
||||
gsize packed;
|
||||
|
||||
if (tone_out)
|
||||
*tone_out = EMOJI_TONE_NONE;
|
||||
if (!sequence || !*sequence)
|
||||
return NULL;
|
||||
|
||||
value = g_hash_table_lookup (emoji_sequence_index (), sequence);
|
||||
if (!value)
|
||||
return NULL;
|
||||
|
||||
packed = GPOINTER_TO_SIZE (value) - 1;
|
||||
if (tone_out)
|
||||
*tone_out = (EmojiTone) (packed % EMOJI_TONE_COUNT);
|
||||
|
||||
return &emoji_entries_table[packed / EMOJI_TONE_COUNT];
|
||||
}
|
||||
|
||||
const char *
|
||||
emoji_data_group_display_name (EmojiGroup group)
|
||||
{
|
||||
static const char *const names[EMOJI_GROUP_COUNT] =
|
||||
{
|
||||
N_("Smileys & Emotion"),
|
||||
N_("People & Body"),
|
||||
N_("Animals & Nature"),
|
||||
N_("Food & Drink"),
|
||||
N_("Travel & Places"),
|
||||
N_("Activities"),
|
||||
N_("Objects"),
|
||||
N_("Symbols"),
|
||||
N_("Flags"),
|
||||
};
|
||||
|
||||
g_return_val_if_fail (group < EMOJI_GROUP_COUNT, "");
|
||||
|
||||
return _(names[group]);
|
||||
}
|
||||
|
||||
const char *
|
||||
emoji_data_group_icon_sequence (EmojiGroup group)
|
||||
{
|
||||
static const char *const icons[EMOJI_GROUP_COUNT] =
|
||||
{
|
||||
"\360\237\230\200", /* 😀 */
|
||||
"\360\237\221\213", /* 👋 */
|
||||
"\360\237\220\273", /* 🐻 */
|
||||
"\360\237\215\224", /* 🍔 */
|
||||
"\360\237\232\227", /* 🚗 */
|
||||
"\342\232\275", /* ⚽ */
|
||||
"\360\237\222\241", /* 💡 */
|
||||
"\360\237\224\243", /* 🔣 */
|
||||
"\360\237\217\201", /* 🏁 */
|
||||
};
|
||||
|
||||
g_return_val_if_fail (group < EMOJI_GROUP_COUNT, "");
|
||||
|
||||
return icons[group];
|
||||
}
|
||||
|
||||
const char *
|
||||
emoji_data_tone_display_name (EmojiTone tone)
|
||||
{
|
||||
static const char *const names[EMOJI_TONE_COUNT] =
|
||||
{
|
||||
N_("No skin tone"),
|
||||
N_("Light skin tone"),
|
||||
N_("Medium-light skin tone"),
|
||||
N_("Medium skin tone"),
|
||||
N_("Medium-dark skin tone"),
|
||||
N_("Dark skin tone"),
|
||||
};
|
||||
|
||||
g_return_val_if_fail (tone < EMOJI_TONE_COUNT, "");
|
||||
|
||||
return _(names[tone]);
|
||||
}
|
||||
|
||||
const char *
|
||||
emoji_entry_sequence_for_tone (const EmojiEntry *entry, EmojiTone tone)
|
||||
{
|
||||
g_return_val_if_fail (entry != NULL, NULL);
|
||||
|
||||
if (tone == EMOJI_TONE_NONE || tone >= EMOJI_TONE_COUNT ||
|
||||
entry->tone_set < 0)
|
||||
return entry->sequence;
|
||||
|
||||
return emoji_tone_sets_table[entry->tone_set].sequence[tone - 1];
|
||||
}
|
||||
|
||||
char **
|
||||
emoji_search_tokenize (const char *text)
|
||||
{
|
||||
GPtrArray *tokens;
|
||||
char *folded;
|
||||
char **split;
|
||||
gsize i;
|
||||
|
||||
if (!text)
|
||||
return NULL;
|
||||
|
||||
folded = g_utf8_casefold (text, -1);
|
||||
split = g_strsplit_set (folded, " \t\n", -1);
|
||||
g_free (folded);
|
||||
|
||||
tokens = g_ptr_array_new ();
|
||||
for (i = 0; split[i]; i++)
|
||||
{
|
||||
const char *token = split[i];
|
||||
gsize len;
|
||||
|
||||
while (*token == ':')
|
||||
token++;
|
||||
len = strlen (token);
|
||||
while (len > 0 && token[len - 1] == ':')
|
||||
len--;
|
||||
if (len > 0)
|
||||
g_ptr_array_add (tokens, g_strndup (token, len));
|
||||
}
|
||||
g_strfreev (split);
|
||||
|
||||
if (tokens->len == 0)
|
||||
{
|
||||
g_ptr_array_free (tokens, TRUE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_ptr_array_add (tokens, NULL);
|
||||
|
||||
return (char **) g_ptr_array_free (tokens, FALSE);
|
||||
}
|
||||
|
||||
gboolean
|
||||
emoji_entry_matches (const EmojiEntry *entry, char *const *needle_tokens)
|
||||
{
|
||||
gsize i;
|
||||
|
||||
g_return_val_if_fail (entry != NULL, FALSE);
|
||||
|
||||
if (!needle_tokens || !needle_tokens[0])
|
||||
return FALSE;
|
||||
|
||||
for (i = 0; needle_tokens[i]; i++)
|
||||
{
|
||||
if (!strstr (entry->search, needle_tokens[i]))
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const char *
|
||||
emoji_data_unicode_version (void)
|
||||
{
|
||||
return emoji_data_unicode_version_str;
|
||||
}
|
||||
|
||||
const char *
|
||||
emoji_data_cldr_version (void)
|
||||
{
|
||||
return emoji_data_cldr_version_str;
|
||||
}
|
||||
91
src/common/emoji-data.h
Normal file
91
src/common/emoji-data.h
Normal file
@@ -0,0 +1,91 @@
|
||||
/* ZoiteChat
|
||||
* Copyright (C) 2026 deepend-tildeclub.
|
||||
*
|
||||
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
/* Bundled emoji catalog, generated at build time from pinned Unicode,
|
||||
* CLDR and gemoji data (see data/emoji/ and tools/gen-emoji-data.py).
|
||||
* Every entry is a fully-qualified Unicode sequence; nothing here depends
|
||||
* on the GTK runtime's emoji data. The catalog only decides what users
|
||||
* can find and insert - rendering stays with the platform's fonts, and
|
||||
* IRC always receives the plain Unicode sequence. */
|
||||
|
||||
#ifndef ZOITECHAT_EMOJI_DATA_H
|
||||
#define ZOITECHAT_EMOJI_DATA_H
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
typedef enum
|
||||
{
|
||||
EMOJI_GROUP_SMILEYS_EMOTION,
|
||||
EMOJI_GROUP_PEOPLE_BODY,
|
||||
EMOJI_GROUP_ANIMALS_NATURE,
|
||||
EMOJI_GROUP_FOOD_DRINK,
|
||||
EMOJI_GROUP_TRAVEL_PLACES,
|
||||
EMOJI_GROUP_ACTIVITIES,
|
||||
EMOJI_GROUP_OBJECTS,
|
||||
EMOJI_GROUP_SYMBOLS,
|
||||
EMOJI_GROUP_FLAGS,
|
||||
EMOJI_GROUP_COUNT
|
||||
} EmojiGroup;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
EMOJI_TONE_NONE,
|
||||
EMOJI_TONE_LIGHT,
|
||||
EMOJI_TONE_MEDIUM_LIGHT,
|
||||
EMOJI_TONE_MEDIUM,
|
||||
EMOJI_TONE_MEDIUM_DARK,
|
||||
EMOJI_TONE_DARK,
|
||||
EMOJI_TONE_COUNT
|
||||
} EmojiTone;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *sequence;
|
||||
const char *name;
|
||||
const char *search;
|
||||
const char *aliases;
|
||||
guint8 group;
|
||||
gint16 tone_set;
|
||||
} EmojiEntry;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *sequence[EMOJI_TONE_COUNT - 1];
|
||||
} EmojiToneSet;
|
||||
|
||||
gsize emoji_data_count (void);
|
||||
const EmojiEntry *emoji_data_entry (gsize index);
|
||||
|
||||
const EmojiEntry *emoji_data_find_by_sequence (const char *sequence,
|
||||
EmojiTone *tone_out);
|
||||
|
||||
const char *emoji_data_group_display_name (EmojiGroup group);
|
||||
const char *emoji_data_group_icon_sequence (EmojiGroup group);
|
||||
const char *emoji_data_tone_display_name (EmojiTone tone);
|
||||
|
||||
const char *emoji_entry_sequence_for_tone (const EmojiEntry *entry,
|
||||
EmojiTone tone);
|
||||
|
||||
char **emoji_search_tokenize (const char *text);
|
||||
gboolean emoji_entry_matches (const EmojiEntry *entry,
|
||||
char *const *needle_tokens);
|
||||
|
||||
const char *emoji_data_unicode_version (void);
|
||||
const char *emoji_data_cldr_version (void);
|
||||
|
||||
#endif
|
||||
@@ -78,6 +78,7 @@ int fe_input_add (int sok, int flags, void *func, void *data);
|
||||
void fe_input_remove (int tag);
|
||||
void fe_idle_add (void *func, void *data);
|
||||
void fe_set_topic (struct session *sess, char *topic, char *stripped_topic);
|
||||
void fe_set_typing (struct session *sess, const char *nick, const char *state);
|
||||
typedef enum
|
||||
{
|
||||
FE_COLOR_NONE = 0,
|
||||
@@ -141,6 +142,7 @@ void fe_set_away (server *serv);
|
||||
void fe_serverlist_open (session *sess);
|
||||
void fe_get_bool (char *title, char *prompt, void *callback, void *userdata);
|
||||
void fe_get_str (char *prompt, char *def, void *callback, void *ud);
|
||||
void fe_get_password (char *prompt, void *callback, void *ud);
|
||||
void fe_get_int (char *prompt, int def, void *callback, void *ud);
|
||||
#define FRF_WRITE 1 /* save file */
|
||||
#define FRF_MULTIPLE 2 /* multi-select */
|
||||
|
||||
71
src/common/gen-public-suffix.py
Normal file
71
src/common/gen-public-suffix.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
URLS = (
|
||||
"https://raw.githubusercontent.com/publicsuffix/list/main/public_suffix_list.dat",
|
||||
"https://publicsuffix.org/list/public_suffix_list.dat",
|
||||
)
|
||||
|
||||
|
||||
def parse_rules(text: str):
|
||||
rules = []
|
||||
for raw in text.splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("//"):
|
||||
continue
|
||||
if " " in line or "\t" in line:
|
||||
line = line.split()[0]
|
||||
rules.append(line.lower())
|
||||
return sorted(set(rules))
|
||||
|
||||
|
||||
def emit_header(path: str, rules):
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as out:
|
||||
out.write("#pragma once\n")
|
||||
out.write("static const char * const public_suffix_rules[] = {\n")
|
||||
for rule in rules:
|
||||
escaped = rule.replace("\\", "\\\\").replace('"', '\\"')
|
||||
out.write(f'\t"{escaped}",\n')
|
||||
out.write("};\n")
|
||||
out.write(
|
||||
"static const unsigned int public_suffix_rules_len = sizeof(public_suffix_rules) / sizeof(public_suffix_rules[0]);\n"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) not in (2, 3):
|
||||
raise SystemExit("usage: gen-public-suffix.py <output> [source]")
|
||||
output = Path(sys.argv[1])
|
||||
sources = []
|
||||
if len(sys.argv) == 3:
|
||||
sources.append(Path(sys.argv[2]))
|
||||
sources.extend(
|
||||
[
|
||||
Path(__file__).with_name("public_suffix_list.dat"),
|
||||
Path("/usr/share/publicsuffix/public_suffix_list.dat"),
|
||||
Path("/app/share/publicsuffix/public_suffix_list.dat"),
|
||||
]
|
||||
)
|
||||
data = None
|
||||
for url in URLS:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=30) as resp:
|
||||
data = resp.read().decode("utf-8")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if data is None:
|
||||
for source in sources:
|
||||
if source.exists():
|
||||
data = source.read_text(encoding="utf-8")
|
||||
break
|
||||
if data is None:
|
||||
raise SystemExit("unable to load public suffix list")
|
||||
rules = parse_rules(data)
|
||||
emit_header(str(output), rules)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -315,6 +315,59 @@ resolve_parent_theme_root (const char *child_theme_root, const char *parent_name
|
||||
return home_local;
|
||||
g_free (home_local);
|
||||
|
||||
{
|
||||
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
|
||||
|
||||
if (xdg_data_home && xdg_data_home[0])
|
||||
{
|
||||
candidate = g_build_filename (xdg_data_home, "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
return candidate;
|
||||
g_free (candidate);
|
||||
}
|
||||
}
|
||||
|
||||
candidate = g_build_filename (g_get_user_data_dir (), "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
return candidate;
|
||||
g_free (candidate);
|
||||
|
||||
{
|
||||
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
|
||||
guint i;
|
||||
|
||||
if (xdg_data_dirs && xdg_data_dirs[0])
|
||||
{
|
||||
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
|
||||
|
||||
for (i = 0; data_dirs && data_dirs[i]; i++)
|
||||
{
|
||||
if (!data_dirs[i][0])
|
||||
continue;
|
||||
candidate = g_build_filename (data_dirs[i], "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
{
|
||||
g_strfreev (data_dirs);
|
||||
return candidate;
|
||||
}
|
||||
g_free (candidate);
|
||||
}
|
||||
g_strfreev (data_dirs);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char *const *system_data_dirs = g_get_system_data_dirs ();
|
||||
|
||||
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
|
||||
{
|
||||
candidate = g_build_filename (system_data_dirs[i], "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
return candidate;
|
||||
g_free (candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
|
||||
candidate = g_build_filename (user_dir, parent_name, NULL);
|
||||
g_free (user_dir);
|
||||
@@ -632,18 +685,55 @@ zoitechat_gtk3_theme_service_discover (void)
|
||||
|
||||
g_mkdir_with_parents (user_dir, 0700);
|
||||
|
||||
user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
|
||||
/* Prefer the live XDG environment over GLib's process-cached copies so
|
||||
* theme lookup matches what GTK resolves for the running session. */
|
||||
{
|
||||
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
|
||||
|
||||
if (xdg_data_home && xdg_data_home[0])
|
||||
user_data_themes = g_build_filename (xdg_data_home, "themes", NULL);
|
||||
else
|
||||
user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
|
||||
}
|
||||
/* Scan the app's own themes dir first: imported themes get a symlink
|
||||
* under the XDG data themes dir so GTK can resolve gtk-theme-name, and
|
||||
* scanning that link first would make the canonical-root dedup keep
|
||||
* the symlink path - changing the theme's path-derived id and breaking
|
||||
* the saved gui_gtk3_theme reference on the next start. */
|
||||
add_theme_root (user_roots, seen_user_roots, user_dir);
|
||||
add_theme_root (user_roots, seen_user_roots, user_data_themes);
|
||||
g_free (user_data_themes);
|
||||
add_theme_root (user_roots, seen_user_roots, home_themes);
|
||||
add_theme_root (user_roots, seen_user_roots, user_dir);
|
||||
|
||||
system_data_dirs = g_get_system_data_dirs ();
|
||||
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
|
||||
{
|
||||
char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL);
|
||||
add_theme_root (system_roots, seen_system_roots, system_themes);
|
||||
g_free (system_themes);
|
||||
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
|
||||
|
||||
if (xdg_data_dirs && xdg_data_dirs[0])
|
||||
{
|
||||
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
|
||||
|
||||
for (i = 0; data_dirs && data_dirs[i]; i++)
|
||||
{
|
||||
char *system_themes;
|
||||
|
||||
if (!data_dirs[i][0])
|
||||
continue;
|
||||
system_themes = g_build_filename (data_dirs[i], "themes", NULL);
|
||||
add_theme_root (system_roots, seen_system_roots, system_themes);
|
||||
g_free (system_themes);
|
||||
}
|
||||
g_strfreev (data_dirs);
|
||||
}
|
||||
else
|
||||
{
|
||||
system_data_dirs = g_get_system_data_dirs ();
|
||||
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
|
||||
{
|
||||
char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL);
|
||||
add_theme_root (system_roots, seen_system_roots, system_themes);
|
||||
g_free (system_themes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < system_roots->len; i++)
|
||||
@@ -922,7 +1012,7 @@ static gboolean
|
||||
validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
{
|
||||
char *index_theme;
|
||||
GKeyFile *keyfile;
|
||||
GKeyFile *keyfile = NULL;
|
||||
char *css_dir;
|
||||
char *css_path;
|
||||
char *raw_inherits;
|
||||
@@ -930,36 +1020,33 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
guint i;
|
||||
GError *load_error = NULL;
|
||||
|
||||
/* index.theme is optional for GTK3 CSS themes (GTK only needs
|
||||
* gtk-3.x/gtk.css), but when it is present it must be well formed so
|
||||
* broken archives are rejected up front. */
|
||||
index_theme = g_build_filename (theme_root, "index.theme", NULL);
|
||||
if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
|
||||
if (g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': missing required index.theme at '%s'.",
|
||||
theme_root, index_theme);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
keyfile = g_key_file_new ();
|
||||
if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.",
|
||||
theme_root, index_theme, load_error->message);
|
||||
g_error_free (load_error);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
keyfile = g_key_file_new ();
|
||||
if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.",
|
||||
theme_root, index_theme, load_error->message);
|
||||
g_error_free (load_error);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!g_key_file_has_group (keyfile, "Desktop Entry"))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.",
|
||||
theme_root, index_theme);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
if (!g_key_file_has_group (keyfile, "Desktop Entry"))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.",
|
||||
theme_root, index_theme);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root);
|
||||
@@ -968,7 +1055,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': could not resolve a GTK CSS directory (expected gtk-3.x/gtk.css).",
|
||||
theme_root);
|
||||
g_key_file_unref (keyfile);
|
||||
if (keyfile)
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -981,15 +1069,20 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
theme_root, css_path);
|
||||
g_free (css_path);
|
||||
g_free (css_dir);
|
||||
g_key_file_unref (keyfile);
|
||||
if (keyfile)
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
g_free (css_path);
|
||||
g_free (css_dir);
|
||||
|
||||
raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
|
||||
g_key_file_unref (keyfile);
|
||||
raw_inherits = NULL;
|
||||
if (keyfile)
|
||||
{
|
||||
raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
|
||||
g_key_file_unref (keyfile);
|
||||
}
|
||||
g_free (index_theme);
|
||||
if (!raw_inherits)
|
||||
return TRUE;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "ignore.h"
|
||||
#include "fe.h"
|
||||
#include "modes.h"
|
||||
#include "network.h"
|
||||
#include "notify.h"
|
||||
#include "outbound.h"
|
||||
#include "inbound.h"
|
||||
@@ -45,6 +46,155 @@
|
||||
#include "servlist.h"
|
||||
#include "sts.h"
|
||||
#include "text.h"
|
||||
|
||||
void reply_state_clear (session *sess);
|
||||
|
||||
gboolean
|
||||
reply_msgid_valid (const char *msgid)
|
||||
{
|
||||
const char *p;
|
||||
|
||||
if (!msgid || !*msgid || *msgid == ':')
|
||||
return FALSE;
|
||||
|
||||
for (p = msgid; *p; p++)
|
||||
{
|
||||
if (*p == ' ' || *p == '\r' || *p == '\n')
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
reply_item_free (reply_item *item)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
g_free (item->msgid);
|
||||
g_free (item->nick);
|
||||
g_free (item->text);
|
||||
g_free (item);
|
||||
}
|
||||
|
||||
void
|
||||
reply_cache_free (session *sess)
|
||||
{
|
||||
if (!sess)
|
||||
return;
|
||||
g_slist_free_full (sess->reply_items, (GDestroyNotify) reply_item_free);
|
||||
sess->reply_items = NULL;
|
||||
reply_state_clear (sess);
|
||||
}
|
||||
|
||||
reply_item *
|
||||
reply_cache_find (session *sess, const char *msgid)
|
||||
{
|
||||
GSList *list;
|
||||
|
||||
if (!sess || !reply_msgid_valid (msgid))
|
||||
return NULL;
|
||||
|
||||
for (list = sess->reply_items; list; list = list->next)
|
||||
{
|
||||
reply_item *item = list->data;
|
||||
if (!strcmp (item->msgid, msgid))
|
||||
return item;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
reply_item *
|
||||
reply_cache_latest_from (session *sess, const char *nick)
|
||||
{
|
||||
GSList *list;
|
||||
|
||||
if (!sess || !nick || !*nick)
|
||||
return NULL;
|
||||
|
||||
for (list = sess->reply_items; list; list = list->next)
|
||||
{
|
||||
reply_item *item = list->data;
|
||||
if (!sess->server->p_cmp (item->nick, nick))
|
||||
return item;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
reply_state_clear (session *sess)
|
||||
{
|
||||
if (!sess)
|
||||
return;
|
||||
g_clear_pointer (&sess->reply_msgid, g_free);
|
||||
g_clear_pointer (&sess->reply_target, g_free);
|
||||
g_clear_pointer (&sess->reply_nick, g_free);
|
||||
g_clear_pointer (&sess->reply_text, g_free);
|
||||
}
|
||||
|
||||
void
|
||||
reply_state_set (session *sess, const char *msgid, const char *target, const char *nick, const char *text)
|
||||
{
|
||||
if (!sess || !reply_msgid_valid (msgid))
|
||||
return;
|
||||
reply_state_clear (sess);
|
||||
sess->reply_msgid = g_strdup (msgid);
|
||||
sess->reply_target = g_strdup (target && *target ? target : sess->channel);
|
||||
sess->reply_nick = g_strdup (nick && *nick ? nick : _("message"));
|
||||
sess->reply_text = g_strdup (text && *text ? text : _("Original message unavailable"));
|
||||
}
|
||||
|
||||
static void
|
||||
reply_cache_add (session *sess, const char *msgid, const char *nick, const char *text, time_t timestamp)
|
||||
{
|
||||
reply_item *item;
|
||||
|
||||
if (!sess || !reply_msgid_valid (msgid) || !nick || !text)
|
||||
return;
|
||||
|
||||
item = reply_cache_find (sess, msgid);
|
||||
if (item)
|
||||
{
|
||||
g_free (item->nick);
|
||||
g_free (item->text);
|
||||
item->nick = g_strdup (nick);
|
||||
item->text = g_strdup (text);
|
||||
item->timestamp = timestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
item = g_new0 (reply_item, 1);
|
||||
item->msgid = g_strdup (msgid);
|
||||
item->nick = g_strdup (nick);
|
||||
item->text = g_strdup (text);
|
||||
item->timestamp = timestamp;
|
||||
sess->reply_items = g_slist_prepend (sess->reply_items, item);
|
||||
|
||||
if (g_slist_length (sess->reply_items) > 200)
|
||||
{
|
||||
GSList *last = g_slist_last (sess->reply_items);
|
||||
item = last->data;
|
||||
sess->reply_items = g_slist_delete_link (sess->reply_items, last);
|
||||
reply_item_free (item);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
reply_context_print (session *sess, const message_tags_data *tags_data)
|
||||
{
|
||||
reply_item *item;
|
||||
|
||||
if (!sess || !tags_data || !tags_data->reply)
|
||||
return;
|
||||
|
||||
item = reply_cache_find (sess, tags_data->reply);
|
||||
if (item)
|
||||
PrintTextTimeStampf (sess, tags_data->timestamp, "\00314│ ↪ %s: %.180s\017\n", item->nick, item->text);
|
||||
else
|
||||
PrintTextTimeStamp (sess, "\00314│ ↪ Original message unavailable\017\n", tags_data->timestamp);
|
||||
}
|
||||
#include "ctcp.h"
|
||||
#include "zoitechatc.h"
|
||||
#include "chanopt.h"
|
||||
@@ -449,6 +599,9 @@ inbound_action (session *sess, char *chan, char *from, char *ip, char *text,
|
||||
fromme = TRUE;
|
||||
}
|
||||
|
||||
if (!fromme)
|
||||
fe_set_typing (sess, from, "done");
|
||||
|
||||
inbound_make_idtext (serv, idtext, sizeof (idtext), id);
|
||||
|
||||
if (!fromme && !privaction)
|
||||
@@ -517,12 +670,17 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
fromme = TRUE;
|
||||
}
|
||||
|
||||
if (!fromme)
|
||||
fe_set_typing (sess, from, "done");
|
||||
|
||||
if (fromme)
|
||||
{
|
||||
if (prefs.hex_away_auto_unmark && serv->is_away && !tags_data->timestamp)
|
||||
sess->server->p_set_back (sess->server);
|
||||
reply_context_print (sess, tags_data);
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL,
|
||||
0, tags_data->timestamp);
|
||||
reply_cache_add (sess, tags_data->msgid, from, text, tags_data->timestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -531,6 +689,8 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
if (is_hilight (from, text, sess, serv))
|
||||
hilight = TRUE;
|
||||
|
||||
reply_context_print (sess, tags_data);
|
||||
|
||||
if (sess->type == SESS_DIALOG)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_DPRIVMSG, sess, from, text, idtext, NULL, 0,
|
||||
tags_data->timestamp);
|
||||
@@ -540,6 +700,8 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CHANMSG, sess, from, text, nickchar, idtext,
|
||||
0, tags_data->timestamp);
|
||||
|
||||
reply_cache_add (sess, tags_data->msgid, from, text, tags_data->timestamp);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1476,14 +1638,13 @@ inbound_uback (server *serv, const message_tags_data *tags_data)
|
||||
void
|
||||
inbound_foundip (session *sess, char *ip, const message_tags_data *tags_data)
|
||||
{
|
||||
struct hostent *HostAddr;
|
||||
struct in_addr addr;
|
||||
|
||||
HostAddr = gethostbyname (ip);
|
||||
if (HostAddr)
|
||||
if (net_lookup_ipv4 (ip, &addr.s_addr))
|
||||
{
|
||||
sess->server->dcc_ip = ((struct in_addr *) HostAddr->h_addr_list[0])->s_addr;
|
||||
sess->server->dcc_ip = addr.s_addr;
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_FOUNDIP, sess->server->server_session,
|
||||
inet_ntoa (*((struct in_addr *) HostAddr->h_addr_list[0])),
|
||||
net_ip (ntohl (addr.s_addr)),
|
||||
NULL, NULL, NULL, 0, tags_data->timestamp);
|
||||
}
|
||||
}
|
||||
@@ -1654,14 +1815,14 @@ inbound_login_end (session *sess, char *text, const message_tags_data *tags_data
|
||||
return;
|
||||
|
||||
/* send nickserv password */
|
||||
if (net && net->pass && inbound_nickserv_login (serv))
|
||||
if (net && serv->password[0] && inbound_nickserv_login (serv))
|
||||
{
|
||||
serv->p_ns_identify (serv, net->pass);
|
||||
serv->p_ns_identify (serv, serv->password);
|
||||
}
|
||||
|
||||
/* wait for join if command or nickserv set */
|
||||
if (net && prefs.hex_irc_join_delay
|
||||
&& ((net->pass && inbound_nickserv_login (serv))
|
||||
&& ((serv->password[0] && inbound_nickserv_login (serv))
|
||||
|| net->commandlist))
|
||||
{
|
||||
serv->joindelay_tag = fe_timeout_add_seconds (prefs.hex_irc_join_delay, check_autojoin_channels, serv);
|
||||
@@ -1742,6 +1903,10 @@ inbound_toggle_caps (server *serv, const char *extensions_str, gboolean enable)
|
||||
serv->have_awaynotify = enable;
|
||||
else if (!strcmp (extension, "account-tag"))
|
||||
serv->have_account_tag = enable;
|
||||
else if (!strcmp (extension, "message-tags"))
|
||||
serv->have_message_tags = enable;
|
||||
else if (!strcmp (extension, "echo-message"))
|
||||
serv->have_echo_message = enable;
|
||||
else if (!strcmp (extension, "sasl"))
|
||||
{
|
||||
serv->have_sasl = enable;
|
||||
@@ -1873,6 +2038,8 @@ static const char * const supported_caps[] = {
|
||||
"account-tag",
|
||||
"extended-monitor",
|
||||
"standard-replies",
|
||||
"message-tags",
|
||||
"echo-message",
|
||||
|
||||
/* ZNC */
|
||||
"znc.in/server-time-iso",
|
||||
|
||||
@@ -78,6 +78,13 @@ void inbound_login_end (session *sess, char *text,
|
||||
void inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
char *text, char fromme, int id,
|
||||
const message_tags_data *tags_data);
|
||||
gboolean reply_msgid_valid (const char *msgid);
|
||||
void reply_cache_free (session *sess);
|
||||
reply_item *reply_cache_find (session *sess, const char *msgid);
|
||||
reply_item *reply_cache_latest_from (session *sess, const char *nick);
|
||||
void reply_state_set (session *sess, const char *msgid, const char *target,
|
||||
const char *nick, const char *text);
|
||||
void reply_state_clear (session *sess);
|
||||
void clear_channel (session *sess);
|
||||
void set_topic (session *sess, char *topic, char *stripped_topic);
|
||||
void inbound_privmsg (server *serv, char *from, char *ip, char *text, int id,
|
||||
|
||||
@@ -3,6 +3,7 @@ common_sources = [
|
||||
'chanopt.c',
|
||||
'ctcp.c',
|
||||
'dcc.c',
|
||||
'emoji-data.c',
|
||||
'gtk3-theme-service.c',
|
||||
'zoitechat.c',
|
||||
'history.c',
|
||||
@@ -27,6 +28,8 @@ common_sources = [
|
||||
'util.c'
|
||||
]
|
||||
|
||||
secretstore_sources = files('secretstore.c')
|
||||
|
||||
common_sysinfo_deps = []
|
||||
libarchive_dep = dependency('libarchive', required: host_machine.system() != 'windows')
|
||||
|
||||
@@ -38,6 +41,9 @@ common_deps = [
|
||||
if libarchive_dep.found()
|
||||
common_deps += libarchive_dep
|
||||
endif
|
||||
if libsecret_dep.found()
|
||||
common_deps += libsecret_dep
|
||||
endif
|
||||
|
||||
common_includes = [
|
||||
config_h_include,
|
||||
@@ -96,6 +102,28 @@ marshal = [
|
||||
|
||||
make_te = find_program('make-te.py')
|
||||
|
||||
|
||||
python3 = find_program('python3', required: true)
|
||||
|
||||
public_suffix_data = custom_target('public_suffix_data_h',
|
||||
output: 'public_suffix_data.h',
|
||||
command: [python3, files('gen-public-suffix.py'), '@OUTPUT@']
|
||||
)
|
||||
|
||||
emoji_data_table = custom_target('emoji_data_table_h',
|
||||
input: [
|
||||
'../../data/emoji/emoji-test.txt',
|
||||
'../../data/emoji/cldr-annotations-en.xml',
|
||||
'../../data/emoji/cldr-annotations-derived-en.xml',
|
||||
'../../data/emoji/gemoji.json',
|
||||
'../../data/emoji/VERSIONS',
|
||||
],
|
||||
output: 'emoji-data-table.h',
|
||||
command: [python3, files('../../tools/gen-emoji-data.py'),
|
||||
'@INPUT0@', '@INPUT1@', '@INPUT2@', '@INPUT3@', '@INPUT4@',
|
||||
'@OUTPUT@']
|
||||
)
|
||||
|
||||
textevents = custom_target('textevents',
|
||||
input: 'textevents.in',
|
||||
output: ['textevents.h', 'textenums.h'],
|
||||
@@ -107,7 +135,7 @@ if libssl_dep.found()
|
||||
common_deps += libssl_dep
|
||||
endif
|
||||
|
||||
if dbus_glib_dep.found()
|
||||
if dbus_dep.found()
|
||||
subdir('dbus')
|
||||
common_deps += zoitechat_dbus_dep
|
||||
common_includes += include_directories('dbus')
|
||||
@@ -119,7 +147,7 @@ if get_option('plugin')
|
||||
endif
|
||||
|
||||
zoitechat_common = static_library('zoitechatcommon',
|
||||
sources: [textevents] + marshal + common_sources,
|
||||
sources: [textevents, public_suffix_data, emoji_data_table] + marshal + common_sources + secretstore_sources,
|
||||
include_directories: config_h_include,
|
||||
dependencies: common_deps + common_sysinfo_deps,
|
||||
c_args: common_cflags,
|
||||
@@ -127,7 +155,7 @@ zoitechat_common = static_library('zoitechatcommon',
|
||||
)
|
||||
|
||||
zoitechat_common_dep = declare_dependency(
|
||||
sources: [textevents] + marshal,
|
||||
sources: [textevents, public_suffix_data] + marshal,
|
||||
link_with: zoitechat_common,
|
||||
include_directories: common_includes,
|
||||
compile_args: common_cflags,
|
||||
@@ -154,3 +182,18 @@ test('GTK3 Theme Service Tests', gtk3_theme_service_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
emoji_data_tests = executable('emoji_data_tests',
|
||||
[
|
||||
'tests/test-emoji-data.c',
|
||||
'emoji-data.c',
|
||||
emoji_data_table,
|
||||
],
|
||||
include_directories: [config_h_include, include_directories('.')],
|
||||
dependencies: [libgio_dep],
|
||||
)
|
||||
|
||||
test('Emoji Data Tests', emoji_data_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
@@ -907,6 +907,10 @@ inbound_005 (server * serv, char *word[], const message_tags_data *tags_data)
|
||||
{
|
||||
if (g_strcmp0 (tokvalue, "ascii") == 0)
|
||||
serv->p_cmp = (void *)g_ascii_strcasecmp;
|
||||
} else if (g_strcmp0 (tokname, "CLIENTTAGDENY") == 0)
|
||||
{
|
||||
g_free (serv->clienttagdeny);
|
||||
serv->clienttagdeny = tokadding ? g_strdup (tokvalue) : NULL;
|
||||
} else if (g_strcmp0 (tokname, "CHARSET") == 0)
|
||||
{
|
||||
if (g_ascii_strcasecmp (tokvalue, "UTF-8") == 0)
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
|
||||
#ifndef WIN32
|
||||
#include <unistd.h>
|
||||
#ifdef HAVE_NETINET_TCP_H
|
||||
#include <netinet/tcp.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define WANTSOCKET
|
||||
@@ -43,6 +46,9 @@
|
||||
|
||||
#define NETWORK_PRIVATE
|
||||
#include "network.h"
|
||||
#include "zoitechat.h"
|
||||
|
||||
extern struct zoitechatprefs prefs;
|
||||
|
||||
#define RAND_INT(n) ((int)(rand() / (RAND_MAX + 1.0) * (n)))
|
||||
|
||||
@@ -58,15 +64,76 @@ net_set_socket_options (int sok)
|
||||
setsockopt (sok, SOL_SOCKET, SO_REUSEADDR, (char *) &sw, sizeof (sw));
|
||||
sw = 1;
|
||||
setsockopt (sok, SOL_SOCKET, SO_KEEPALIVE, (char *) &sw, sizeof (sw));
|
||||
#ifdef TCP_KEEPIDLE
|
||||
{
|
||||
int keepidle = prefs.hex_net_keepalive_idle;
|
||||
if (keepidle > 0)
|
||||
setsockopt (sok, IPPROTO_TCP, TCP_KEEPIDLE, (char *) &keepidle, sizeof (keepidle));
|
||||
}
|
||||
#endif
|
||||
#ifdef TCP_KEEPINTVL
|
||||
{
|
||||
int keepintvl = prefs.hex_net_keepalive_interval;
|
||||
if (keepintvl > 0)
|
||||
setsockopt (sok, IPPROTO_TCP, TCP_KEEPINTVL, (char *) &keepintvl, sizeof (keepintvl));
|
||||
}
|
||||
#endif
|
||||
#ifdef TCP_KEEPCNT
|
||||
{
|
||||
int keepcnt = prefs.hex_net_keepalive_count;
|
||||
if (keepcnt > 0)
|
||||
setsockopt (sok, IPPROTO_TCP, TCP_KEEPCNT, (char *) &keepcnt, sizeof (keepcnt));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
char *
|
||||
net_ip (uint32_t addr)
|
||||
{
|
||||
static char buf[INET_ADDRSTRLEN];
|
||||
struct in_addr ia;
|
||||
|
||||
ia.s_addr = htonl (addr);
|
||||
return inet_ntoa (ia);
|
||||
if (!inet_ntop (AF_INET, &ia, buf, sizeof (buf)))
|
||||
buf[0] = 0;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
int
|
||||
net_parse_ipv4 (const char *hostname, uint32_t *addr)
|
||||
{
|
||||
struct in_addr ia;
|
||||
|
||||
if (inet_pton (AF_INET, hostname, &ia) != 1)
|
||||
return FALSE;
|
||||
|
||||
*addr = ia.s_addr;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int
|
||||
net_lookup_ipv4 (const char *hostname, uint32_t *addr)
|
||||
{
|
||||
struct addrinfo hints;
|
||||
struct addrinfo *res;
|
||||
struct sockaddr_in *sin;
|
||||
int ret;
|
||||
|
||||
memset (&hints, 0, sizeof (hints));
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_flags = AI_ADDRCONFIG;
|
||||
|
||||
ret = getaddrinfo (hostname, NULL, &hints, &res);
|
||||
if (ret != 0)
|
||||
return FALSE;
|
||||
|
||||
sin = (struct sockaddr_in *) res->ai_addr;
|
||||
*addr = sin->sin_addr.s_addr;
|
||||
freeaddrinfo (res);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -39,6 +39,8 @@ int net_connect (netstore *ns, int sok4, int sok6, int *sok_return);
|
||||
char *net_resolve (netstore *ns, char *hostname, int port, char **real_host);
|
||||
void net_bind (netstore *tobindto, int sok4, int sok6);
|
||||
char *net_ip (uint32_t addr);
|
||||
int net_parse_ipv4 (const char *hostname, uint32_t *addr);
|
||||
int net_lookup_ipv4 (const char *hostname, uint32_t *addr);
|
||||
void net_sockets (int *sok4, int *sok6);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -468,7 +468,7 @@ create_mask (session * sess, char *mask, char *mode, char *typestr, int deop)
|
||||
type = prefs.hex_irc_ban_type;
|
||||
|
||||
buf[0] = 0;
|
||||
if (inet_addr (fullhost) != (guint32) -1) /* "fullhost" is really a IP number */
|
||||
if (net_parse_ipv4 (fullhost, &(guint32){0})) /* "fullhost" is really a IP number */
|
||||
{
|
||||
lastdot = strrchr (fullhost, '.');
|
||||
if (!lastdot)
|
||||
@@ -2744,8 +2744,8 @@ cmd_me (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
while ((split_text = split_up_text (sess, act + offset, cmd_length, split_text)))
|
||||
{
|
||||
sess->server->p_action (sess->server, sess->channel, split_text);
|
||||
/* print it to screen */
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
split_text, TRUE, FALSE,
|
||||
&no_tags);
|
||||
|
||||
@@ -2756,8 +2756,8 @@ cmd_me (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
}
|
||||
|
||||
sess->server->p_action (sess->server, sess->channel, act + offset);
|
||||
/* print it to screen */
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
act + offset, TRUE, FALSE, &no_tags);
|
||||
} else
|
||||
{
|
||||
@@ -2821,6 +2821,137 @@ cmd_mop (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
static gboolean
|
||||
client_tag_allowed (server *serv, const char *tag)
|
||||
{
|
||||
char **deny;
|
||||
int i;
|
||||
|
||||
if (!serv->have_message_tags)
|
||||
return FALSE;
|
||||
|
||||
if (!serv->clienttagdeny || !*serv->clienttagdeny)
|
||||
return TRUE;
|
||||
|
||||
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
||||
for (i = 0; deny[i]; i++)
|
||||
{
|
||||
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
{
|
||||
g_strfreev (deny);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
g_strfreev (deny);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static char *
|
||||
client_tag_escape (const char *text)
|
||||
{
|
||||
GString *out;
|
||||
const char *p;
|
||||
|
||||
out = g_string_sized_new (strlen (text));
|
||||
for (p = text; *p; p++)
|
||||
{
|
||||
switch (*p)
|
||||
{
|
||||
case ';':
|
||||
g_string_append (out, "\\:");
|
||||
break;
|
||||
case ' ':
|
||||
g_string_append (out, "\\s");
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append (out, "\\\\");
|
||||
break;
|
||||
case '\r':
|
||||
g_string_append (out, "\\r");
|
||||
break;
|
||||
case '\n':
|
||||
g_string_append (out, "\\n");
|
||||
break;
|
||||
default:
|
||||
g_string_append_c (out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return g_string_free (out, FALSE);
|
||||
}
|
||||
|
||||
static int
|
||||
cmd_reply (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
{
|
||||
char *msgid = word[2];
|
||||
char *target = sess->channel;
|
||||
char *text = word_eol[3];
|
||||
char *escaped;
|
||||
char *tags;
|
||||
|
||||
if (!reply_msgid_valid (msgid) || !*target || !*text)
|
||||
return FALSE;
|
||||
|
||||
if (*text == ':')
|
||||
text++;
|
||||
|
||||
if (!sess->server->connected || !client_tag_allowed (sess->server, "reply"))
|
||||
{
|
||||
notc_msg (sess);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
escaped = client_tag_escape (msgid);
|
||||
tags = g_strdup_printf ("+reply=%s", escaped);
|
||||
sess->server->p_message_tagged (sess->server, tags, target, text);
|
||||
if (!sess->server->have_echo_message)
|
||||
{
|
||||
session *target_sess = find_dialog (sess->server, target);
|
||||
message_tags_data no_tags = MESSAGE_TAGS_DATA_INIT;
|
||||
no_tags.reply = msgid;
|
||||
|
||||
if (!target_sess)
|
||||
target_sess = find_channel (sess->server, target);
|
||||
if (target_sess)
|
||||
inbound_chanmsg (target_sess->server, target_sess, target_sess->channel, target_sess->server->nick, text, TRUE, FALSE, &no_tags);
|
||||
}
|
||||
g_free (tags);
|
||||
g_free (escaped);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static int
|
||||
cmd_typing (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
{
|
||||
char *state = word[2];
|
||||
char *target = word[3];
|
||||
char tags[32];
|
||||
|
||||
if (!*state)
|
||||
state = "active";
|
||||
|
||||
if (!*target)
|
||||
target = sess->channel;
|
||||
|
||||
if (!*target || (strcmp (state, "active") && strcmp (state, "paused") && strcmp (state, "done")))
|
||||
return FALSE;
|
||||
|
||||
if (!sess->server->connected || !client_tag_allowed (sess->server, "typing"))
|
||||
{
|
||||
notc_msg (sess);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
|
||||
sess->server->p_tagmsg (sess->server, tags, target);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static int
|
||||
cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
{
|
||||
@@ -2875,7 +3006,7 @@ cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
newsess = find_dialog (sess->server, nick);
|
||||
if (!newsess)
|
||||
newsess = find_channel (sess->server, nick);
|
||||
if (newsess)
|
||||
if (newsess && !sess->server->have_echo_message)
|
||||
{
|
||||
message_tags_data no_tags = MESSAGE_TAGS_DATA_INIT;
|
||||
|
||||
@@ -3462,28 +3593,45 @@ cmd_server (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
int use_ssl = FALSE;
|
||||
#endif
|
||||
int is_url = TRUE;
|
||||
int no_proxy = FALSE;
|
||||
server *serv = sess->server;
|
||||
ircnet *net = NULL;
|
||||
|
||||
while (TRUE)
|
||||
{
|
||||
#ifdef USE_OPENSSL
|
||||
/* BitchX uses -ssl, mIRC uses -e, let's support both */
|
||||
if (g_strcmp0 (word[2], "-ssl") == 0 || g_strcmp0 (word[2], "-e") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
offset++; /* args move up by 1 word */
|
||||
}
|
||||
else if (g_strcmp0 (word[2], "-ssl-noverify") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
use_ssl_noverify = TRUE;
|
||||
offset++; /* args move up by 1 word */
|
||||
}
|
||||
else if (g_strcmp0 (word[2], "-insecure") == 0)
|
||||
{
|
||||
use_ssl = FALSE;
|
||||
offset++; /* args move up by 1 word */
|
||||
}
|
||||
if (g_strcmp0 (word[2 + offset], "-ssl") == 0 || g_strcmp0 (word[2 + offset], "-e") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
use_ssl_noverify = FALSE;
|
||||
offset++;
|
||||
}
|
||||
else if (g_strcmp0 (word[2 + offset], "-ssl-noverify") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
use_ssl_noverify = TRUE;
|
||||
offset++;
|
||||
}
|
||||
else if (g_strcmp0 (word[2 + offset], "-insecure") == 0)
|
||||
{
|
||||
use_ssl = FALSE;
|
||||
use_ssl_noverify = FALSE;
|
||||
offset++;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
if (g_strcmp0 (word[2 + offset], "-noproxy") == 0)
|
||||
{
|
||||
no_proxy = TRUE;
|
||||
offset++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
serv->dont_use_proxy = no_proxy;
|
||||
|
||||
if (!parse_irc_url (word[2 + offset], &server_name, &port, &channel, &key, &use_ssl))
|
||||
{
|
||||
@@ -3584,10 +3732,18 @@ cmd_servchan (struct session *sess, char *tbuf, char *word[],
|
||||
{
|
||||
int offset = 0;
|
||||
|
||||
while (TRUE)
|
||||
{
|
||||
#ifdef USE_OPENSSL
|
||||
if (g_strcmp0 (word[2], "-ssl") == 0 || g_strcmp0 (word[2], "-ssl-noverify") == 0 || g_strcmp0 (word[2], "-insecure") == 0)
|
||||
offset++;
|
||||
if (g_strcmp0 (word[2 + offset], "-ssl") == 0 || g_strcmp0 (word[2 + offset], "-ssl-noverify") == 0 || g_strcmp0 (word[2 + offset], "-insecure") == 0)
|
||||
offset++;
|
||||
else
|
||||
#endif
|
||||
if (g_strcmp0 (word[2 + offset], "-noproxy") == 0)
|
||||
offset++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (*word[4 + offset])
|
||||
{
|
||||
@@ -4114,22 +4270,24 @@ const struct commands xc_cmds[] = {
|
||||
#endif
|
||||
{"RECV", cmd_recv, 1, 0, 1, N_("RECV <text>, send raw data to ZoiteChat, as if it was received from the IRC server")},
|
||||
{"RELOAD", cmd_reload, 0, 0, 1, N_("RELOAD <name>, reloads a plugin or script")},
|
||||
{"REPLY", cmd_reply, 0, 0, 1, N_("REPLY <msgid> <message>, sends a reply-tagged message to the current channel or dialog")},
|
||||
{"SAY", cmd_say, 0, 0, 1,
|
||||
N_("SAY <text>, sends the text to the object in the current window")},
|
||||
{"SEND", cmd_send, 0, 0, 1, N_("SEND <nick> [<file>]")},
|
||||
{"TYPING", cmd_typing, 0, 0, 1, N_("TYPING [active|paused|done] [target], sends a typing notification")},
|
||||
#ifdef USE_OPENSSL
|
||||
{"SERVCHAN", cmd_servchan, 0, 0, 1,
|
||||
N_("SERVCHAN [-insecure|-ssl|-ssl-noverify] <host> <port> <channel>, connects and joins a channel using ssl unless otherwise specified")},
|
||||
N_("SERVCHAN [-noproxy] [-insecure|-ssl|-ssl-noverify] <host> <port> <channel>, connects and joins a channel using ssl unless otherwise specified")},
|
||||
#else
|
||||
{"SERVCHAN", cmd_servchan, 0, 0, 1,
|
||||
N_("SERVCHAN <host> <port> <channel>, connects and joins a channel")},
|
||||
N_("SERVCHAN [-noproxy] <host> <port> <channel>, connects and joins a channel")},
|
||||
#endif
|
||||
#ifdef USE_OPENSSL
|
||||
{"SERVER", cmd_server, 0, 0, 1,
|
||||
N_("SERVER [-insecure|-ssl|-ssl-noverify] <host> [<port>] [<password>], connects to a server using ssl unless otherwise specified, the default port is 6697 for ssl connections and 6667 for insecure connections")},
|
||||
N_("SERVER [-noproxy] [-insecure|-ssl|-ssl-noverify] <host> [<port>] [<password>], connects to a server using ssl unless otherwise specified, the default port is 6697 for ssl connections and 6667 for insecure connections")},
|
||||
#else
|
||||
{"SERVER", cmd_server, 0, 0, 1,
|
||||
N_("SERVER <host> [<port>] [<password>], connects to a server, the default port is 6667")},
|
||||
N_("SERVER [-noproxy] <host> [<port>] [<password>], connects to a server, the default port is 6667")},
|
||||
#endif
|
||||
{"SET", cmd_set, 0, 0, 1, N_("SET [-e] [-off|-on] [-quiet] <variable> [<value>]")},
|
||||
{"SETCURSOR", cmd_setcursor, 0, 0, 1, N_("SETCURSOR [-|+]<position>, reposition the cursor in the inputbox")},
|
||||
@@ -4166,10 +4324,10 @@ const struct commands xc_cmds[] = {
|
||||
static int
|
||||
command_compare (const void *a, const void *b)
|
||||
{
|
||||
return g_ascii_strcasecmp (a, ((struct commands *)b)->name);
|
||||
return g_ascii_strcasecmp (a, ((const struct commands *)b)->name);
|
||||
}
|
||||
|
||||
static struct commands *
|
||||
static const struct commands *
|
||||
find_internal_command (char *name)
|
||||
{
|
||||
/* the "-1" is to skip the NULL terminator */
|
||||
@@ -4205,7 +4363,7 @@ usercommand_show_help (session *sess, char *name)
|
||||
static void
|
||||
help (session *sess, char *tbuf, char *helpcmd, int quiet)
|
||||
{
|
||||
struct commands *cmd;
|
||||
const struct commands *cmd;
|
||||
|
||||
if (plugin_show_help (sess, helpcmd))
|
||||
return;
|
||||
@@ -4397,7 +4555,7 @@ void
|
||||
check_special_chars (char *cmd, int do_ascii) /* check for %X */
|
||||
{
|
||||
int occur = 0;
|
||||
int len = strlen (cmd);
|
||||
size_t len = strlen (cmd);
|
||||
char *buf, *utf;
|
||||
char tbuf[4];
|
||||
int i = 0, j = 0;
|
||||
@@ -4649,8 +4807,9 @@ handle_say (session *sess, char *text, int check_spch)
|
||||
|
||||
while ((split_text = split_up_text (sess, text + offset, cmd_length, split_text)))
|
||||
{
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
split_text, TRUE, FALSE, &no_tags);
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
split_text, TRUE, FALSE, &no_tags);
|
||||
sess->server->p_message (sess->server, sess->channel, split_text);
|
||||
|
||||
if (*split_text)
|
||||
@@ -4659,7 +4818,8 @@ handle_say (session *sess, char *text, int check_spch)
|
||||
g_free (split_text);
|
||||
}
|
||||
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
text + offset, TRUE, FALSE, &no_tags);
|
||||
sess->server->p_message (sess->server, sess->channel, text + offset);
|
||||
} else
|
||||
@@ -4708,9 +4868,9 @@ command_insert_vars (session *sess, char *cmd)
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
if (mynet->pass)
|
||||
if (sess->server->password[0])
|
||||
{
|
||||
g_string_append (expanded, mynet->pass);
|
||||
g_string_append (expanded, sess->server->password);
|
||||
}
|
||||
cmd++;
|
||||
break;
|
||||
@@ -4763,7 +4923,7 @@ handle_command (session *sess, char *cmd, int check_spch)
|
||||
char *word[PDIWORDS+1];
|
||||
char *word_eol[PDIWORDS+1];
|
||||
static int command_level = 0;
|
||||
struct commands *int_cmd;
|
||||
const struct commands *int_cmd;
|
||||
char *pdibuf;
|
||||
char *tbuf;
|
||||
int len;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
#ifdef WIN32
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
@@ -56,6 +57,36 @@ typedef struct session zoitechat_context;
|
||||
#endif
|
||||
|
||||
#define DEBUG(x) {x;}
|
||||
#ifdef WIN32
|
||||
|
||||
/* Python >= 3.9 dropped support for Windows versions before 10, so those
|
||||
* systems get a Python 3.8 build of the plugin instead. RtlGetVersion is
|
||||
* used because GetVersionEx reports a version capped by the compatibility
|
||||
* manifest rather than the real one. */
|
||||
static gboolean
|
||||
plugin_windows_needs_python38 (void)
|
||||
{
|
||||
typedef LONG (WINAPI *rtl_get_version_func) (PRTL_OSVERSIONINFOW);
|
||||
rtl_get_version_func rtl_get_version;
|
||||
RTL_OSVERSIONINFOW version;
|
||||
HMODULE ntdll;
|
||||
|
||||
ntdll = GetModuleHandleW (L"ntdll.dll");
|
||||
if (ntdll == NULL)
|
||||
return FALSE;
|
||||
|
||||
rtl_get_version = (rtl_get_version_func) GetProcAddress (ntdll, "RtlGetVersion");
|
||||
if (rtl_get_version == NULL)
|
||||
return FALSE;
|
||||
|
||||
memset (&version, 0, sizeof (version));
|
||||
version.dwOSVersionInfoSize = sizeof (version);
|
||||
if (rtl_get_version (&version) != 0)
|
||||
return FALSE;
|
||||
|
||||
return version.dwMajorVersion < 10;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* crafted to be an even 32 bytes */
|
||||
struct _zoitechat_hook
|
||||
@@ -491,9 +522,11 @@ plugin_auto_load (session *sess)
|
||||
for_files (lib_dir, "hcfishlim.dll", plugin_auto_load_cb);
|
||||
for_files(lib_dir, "hclua.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcperl.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb);
|
||||
if (plugin_windows_needs_python38 ())
|
||||
for_files (lib_dir, "hcpython38.dll", plugin_auto_load_cb);
|
||||
else
|
||||
for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcupd.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcwinamp.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcsysinfo.dll", plugin_auto_load_cb);
|
||||
#else
|
||||
for_files (lib_dir, "*."PLUGIN_SUFFIX, plugin_auto_load_cb);
|
||||
@@ -1214,8 +1247,6 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
|
||||
|
||||
case 0x4889ba9b: /* password */
|
||||
case 0x438fdf9: /* nickserv */
|
||||
if (sess->server->network)
|
||||
return ((ircnet *)sess->server->network)->pass;
|
||||
return NULL;
|
||||
|
||||
case 0xca022f43: /* server */
|
||||
|
||||
@@ -362,6 +362,18 @@ irc_message (server *serv, char *channel, char *text)
|
||||
tcp_sendf (serv, "PRIVMSG %s :%s\r\n", channel, text);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_message_tagged (server *serv, char *tags, char *channel, char *text)
|
||||
{
|
||||
tcp_sendf (serv, "@%s PRIVMSG %s :%s\r\n", tags, channel, text);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_tagmsg (server *serv, char *tags, char *target)
|
||||
{
|
||||
tcp_sendf (serv, "@%s TAGMSG %s\r\n", tags, target);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_action (server *serv, char *channel, char *act)
|
||||
{
|
||||
@@ -425,7 +437,7 @@ static int
|
||||
irc_raw (server *serv, char *raw)
|
||||
{
|
||||
int len;
|
||||
char tbuf[4096];
|
||||
char tbuf[8704];
|
||||
if (*raw)
|
||||
{
|
||||
len = strlen (raw);
|
||||
@@ -1009,6 +1021,40 @@ process_numeric (session * sess, int n,
|
||||
|
||||
/* handle named messages that starts with a ':' */
|
||||
|
||||
|
||||
static void
|
||||
emit_standard_reply (session *sess, const char *type, char *command, char *code, char *text, const message_tags_data *tags_data)
|
||||
{
|
||||
int is_global = g_strcmp0 (command, "*") == 0;
|
||||
|
||||
if (!text)
|
||||
text = "";
|
||||
if (*text == ':')
|
||||
text++;
|
||||
|
||||
if (!strcmp (type, "FAIL"))
|
||||
{
|
||||
if (is_global)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_FAIL, sess, code, text, NULL, NULL, NULL, tags_data->timestamp);
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_FAILCMD, sess, command, code, text, NULL, NULL, tags_data->timestamp);
|
||||
}
|
||||
else if (!strcmp (type, "WARN"))
|
||||
{
|
||||
if (is_global)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_WARN, sess, code, text, NULL, NULL, NULL, tags_data->timestamp);
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_WARNCMD, sess, command, code, text, NULL, NULL, tags_data->timestamp);
|
||||
}
|
||||
else if (!strcmp (type, "NOTE"))
|
||||
{
|
||||
if (is_global)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_NOTE, sess, code, text, NULL, NULL, NULL, tags_data->timestamp);
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_NOTECMD, sess, command, code, text, NULL, NULL, tags_data->timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
||||
const message_tags_data *tags_data)
|
||||
@@ -1017,7 +1063,7 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
||||
char *account;
|
||||
char ip[128], nick[NICKLEN];
|
||||
char *text, *ex;
|
||||
int len = strlen (type);
|
||||
size_t len = strlen (type);
|
||||
|
||||
/* fill in the "ip" and "nick" buffers */
|
||||
ex = strchr (word[1], '!');
|
||||
@@ -1322,7 +1368,20 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
||||
{
|
||||
if (ignore_check (word[1], IG_PRIV))
|
||||
return;
|
||||
inbound_privmsg (serv, nick, ip, text, tags_data->identified, tags_data);
|
||||
if (serv->have_echo_message && !serv->p_cmp (nick, serv->nick))
|
||||
{
|
||||
session *target_sess = find_dialog (serv, to);
|
||||
|
||||
if (!target_sess)
|
||||
target_sess = find_channel (serv, to);
|
||||
if (target_sess)
|
||||
inbound_chanmsg (serv, target_sess, target_sess->channel, nick, text, TRUE, tags_data->identified, tags_data);
|
||||
else if (serv->front_session)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_MSGSEND, serv->front_session, to, text, NULL, NULL, 0, tags_data->timestamp);
|
||||
} else
|
||||
{
|
||||
inbound_privmsg (serv, nick, ip, text, tags_data->identified, tags_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1335,6 +1394,29 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
||||
tags_data);
|
||||
return;
|
||||
|
||||
case WORDL('T','A','G','M'):
|
||||
if (tags_data->typing)
|
||||
{
|
||||
session *typing_sess = NULL;
|
||||
char *to = word[3];
|
||||
|
||||
if (is_channel (serv, to))
|
||||
typing_sess = find_channel (serv, to);
|
||||
else if (!serv->p_cmp (to, serv->nick))
|
||||
typing_sess = find_dialog (serv, nick);
|
||||
if (!typing_sess)
|
||||
typing_sess = sess;
|
||||
if (!serv->p_cmp (nick, serv->nick))
|
||||
return;
|
||||
if (!strcmp (tags_data->typing, "done"))
|
||||
fe_set_typing (typing_sess, nick, "done");
|
||||
else if (!strcmp (tags_data->typing, "paused"))
|
||||
fe_set_typing (typing_sess, nick, "paused");
|
||||
else if (!strcmp (tags_data->typing, "active"))
|
||||
fe_set_typing (typing_sess, nick, "active");
|
||||
}
|
||||
return;
|
||||
|
||||
case WORDL('W','A','L','L'):
|
||||
text = word_eol[3];
|
||||
if (*text == ':')
|
||||
@@ -1431,6 +1513,11 @@ process_named_servermsg (session *sess, char *buf, char *rawname, char *word_eol
|
||||
inbound_sasl_authenticate (sess->server, word_eol[2]);
|
||||
return;
|
||||
}
|
||||
if (!strncmp (buf, "FAIL ", 5) || !strncmp (buf, "WARN ", 5) || !strncmp (buf, "NOTE ", 5))
|
||||
{
|
||||
emit_standard_reply (sess, word_eol[1], word_eol[2], word_eol[3], word_eol[4], tags_data);
|
||||
return;
|
||||
}
|
||||
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_SERVTEXT, sess, buf, sess->server->servername,
|
||||
rawname, NULL, 0, tags_data->timestamp);
|
||||
@@ -1533,39 +1620,130 @@ handle_message_tag_time (const char *time, message_tags_data *tags_data)
|
||||
*
|
||||
* See http://ircv3.atheme.org/specification/message-tags-3.2
|
||||
*/
|
||||
static char *
|
||||
message_tag_unescape (const char *value)
|
||||
{
|
||||
GString *out;
|
||||
const char *p;
|
||||
|
||||
if (!*value)
|
||||
return NULL;
|
||||
|
||||
out = g_string_sized_new (strlen (value));
|
||||
|
||||
for (p = value; *p; p++)
|
||||
{
|
||||
if (*p != '\\')
|
||||
{
|
||||
g_string_append_c (out, *p);
|
||||
continue;
|
||||
}
|
||||
|
||||
p++;
|
||||
if (!*p)
|
||||
break;
|
||||
|
||||
switch (*p)
|
||||
{
|
||||
case ':':
|
||||
g_string_append_c (out, ';');
|
||||
break;
|
||||
case 's':
|
||||
g_string_append_c (out, ' ');
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append_c (out, '\\');
|
||||
break;
|
||||
case 'r':
|
||||
g_string_append_c (out, '\r');
|
||||
break;
|
||||
case 'n':
|
||||
g_string_append_c (out, '\n');
|
||||
break;
|
||||
default:
|
||||
g_string_append_c (out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
value = out->str;
|
||||
if (!g_utf8_validate (value, -1, NULL))
|
||||
{
|
||||
g_string_free (out, TRUE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return g_string_free (out, FALSE);
|
||||
}
|
||||
|
||||
static void
|
||||
handle_message_tags (server *serv, const char *tags_str,
|
||||
message_tags_data *tags_data)
|
||||
{
|
||||
char **tags;
|
||||
char *time = NULL;
|
||||
int i;
|
||||
|
||||
/* FIXME We might want to avoid the allocation overhead here since
|
||||
* this might be called for every message from the server.
|
||||
*/
|
||||
tags = g_strsplit (tags_str, ";", 0);
|
||||
|
||||
for (i=0; tags[i]; i++)
|
||||
for (i = 0; tags[i]; i++)
|
||||
{
|
||||
char *key = tags[i];
|
||||
char *value = strchr (tags[i], '=');
|
||||
char *raw_value = strchr (tags[i], '=');
|
||||
char *value = NULL;
|
||||
|
||||
if (!value)
|
||||
if (!*key)
|
||||
continue;
|
||||
|
||||
*value = '\0';
|
||||
value++;
|
||||
if (raw_value)
|
||||
{
|
||||
*raw_value = '\0';
|
||||
raw_value++;
|
||||
value = message_tag_unescape (raw_value);
|
||||
}
|
||||
|
||||
if (serv->have_account_tag && !strcmp (key, "account"))
|
||||
tags_data->account = g_strdup (value);
|
||||
|
||||
if (serv->have_idmsg && strcmp (key, "solanum.chat/identified"))
|
||||
{
|
||||
g_free (tags_data->account);
|
||||
tags_data->account = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (serv->have_idmsg && !strcmp (key, "solanum.chat/identified"))
|
||||
{
|
||||
tags_data->identified = TRUE;
|
||||
}
|
||||
else if (serv->have_server_time && !strcmp (key, "time"))
|
||||
{
|
||||
g_free (time);
|
||||
time = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (!strcmp (key, "msgid"))
|
||||
{
|
||||
g_free (tags_data->msgid);
|
||||
tags_data->msgid = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (!strcmp (key, "+reply"))
|
||||
{
|
||||
g_free (tags_data->reply);
|
||||
tags_data->reply = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (!strcmp (key, "+typing"))
|
||||
{
|
||||
g_free (tags_data->typing);
|
||||
tags_data->typing = value;
|
||||
value = NULL;
|
||||
}
|
||||
|
||||
if (serv->have_server_time && !strcmp (key, "time"))
|
||||
handle_message_tag_time (value, tags_data);
|
||||
g_free (value);
|
||||
}
|
||||
|
||||
|
||||
if (time)
|
||||
handle_message_tag_time (time, tags_data);
|
||||
|
||||
g_free (time);
|
||||
g_strfreev (tags);
|
||||
}
|
||||
|
||||
@@ -1667,6 +1845,9 @@ void
|
||||
message_tags_data_free (message_tags_data *tags_data)
|
||||
{
|
||||
g_clear_pointer (&tags_data->account, g_free);
|
||||
g_clear_pointer (&tags_data->msgid, g_free);
|
||||
g_clear_pointer (&tags_data->reply, g_free);
|
||||
g_clear_pointer (&tags_data->typing, g_free);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1696,6 +1877,8 @@ proto_fill_her_up (server *serv)
|
||||
serv->p_set_back = irc_set_back;
|
||||
serv->p_set_away = irc_set_away;
|
||||
serv->p_message = irc_message;
|
||||
serv->p_message_tagged = irc_message_tagged;
|
||||
serv->p_tagmsg = irc_tagmsg;
|
||||
serv->p_action = irc_action;
|
||||
serv->p_notice = irc_notice;
|
||||
serv->p_topic = irc_topic;
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
NULL, /* account name */ \
|
||||
FALSE, /* identified to nick */ \
|
||||
(time_t)0, /* timestamp */ \
|
||||
NULL, \
|
||||
NULL, \
|
||||
NULL, \
|
||||
}
|
||||
|
||||
#define STRIP_COLON(word, word_eol, idx) (word)[(idx)][0] == ':' ? (word_eol)[(idx)]+1 : (word)[(idx)]
|
||||
@@ -41,6 +44,9 @@ typedef struct
|
||||
char *account;
|
||||
gboolean identified;
|
||||
time_t timestamp;
|
||||
char *msgid;
|
||||
char *reply;
|
||||
char *typing;
|
||||
} message_tags_data;
|
||||
|
||||
void message_tags_data_free (message_tags_data *tags_data);
|
||||
|
||||
173
src/common/secretstore.c
Normal file
173
src/common/secretstore.c
Normal file
@@ -0,0 +1,173 @@
|
||||
#include "zoitechat.h"
|
||||
#include "cfgfiles.h"
|
||||
#include "secretstore.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <windows.h>
|
||||
#include <wincred.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBSECRET
|
||||
#include <libsecret/secret.h>
|
||||
#endif
|
||||
|
||||
static char *secretstore_target (const char *network_name)
|
||||
{
|
||||
return g_strdup_printf ("zoitechat/network/%s", network_name ? network_name : "default");
|
||||
}
|
||||
|
||||
int secretstore_is_keyring_available (void)
|
||||
{
|
||||
#ifdef WIN32
|
||||
return TRUE;
|
||||
#elif defined(HAVE_LIBSECRET)
|
||||
return TRUE;
|
||||
#else
|
||||
return FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
char *secretstore_get_network_password (const char *network_name)
|
||||
{
|
||||
char *target;
|
||||
target = secretstore_target (network_name);
|
||||
#ifdef WIN32
|
||||
{
|
||||
PCREDENTIALA cred = NULL;
|
||||
char *ret = NULL;
|
||||
if (CredReadA (target, CRED_TYPE_GENERIC, 0, &cred))
|
||||
{
|
||||
ret = g_strndup ((const char *) cred->CredentialBlob, cred->CredentialBlobSize);
|
||||
CredFree (cred);
|
||||
}
|
||||
g_free (target);
|
||||
return ret;
|
||||
}
|
||||
#elif defined(HAVE_LIBSECRET)
|
||||
{
|
||||
static const SecretSchema schema = {
|
||||
"net.zoite.ZoiteChat.Network", SECRET_SCHEMA_NONE,
|
||||
{
|
||||
{ "network", SECRET_SCHEMA_ATTRIBUTE_STRING },
|
||||
{ NULL, 0 },
|
||||
}
|
||||
};
|
||||
char *ret = secret_password_lookup_sync (&schema, NULL, NULL, "network", target, NULL);
|
||||
g_free (target);
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
g_free (target);
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
int secretstore_set_network_password (const char *network_name, const char *password)
|
||||
{
|
||||
char *target;
|
||||
target = secretstore_target (network_name);
|
||||
#ifdef WIN32
|
||||
{
|
||||
CREDENTIALA cred;
|
||||
memset (&cred, 0, sizeof (cred));
|
||||
cred.Type = CRED_TYPE_GENERIC;
|
||||
cred.TargetName = target;
|
||||
cred.CredentialBlobSize = (DWORD) strlen (password);
|
||||
cred.CredentialBlob = (LPBYTE) password;
|
||||
cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
|
||||
cred.UserName = "zoitechat";
|
||||
if (!CredWriteA (&cred, 0))
|
||||
{
|
||||
g_free (target);
|
||||
return FALSE;
|
||||
}
|
||||
g_free (target);
|
||||
return TRUE;
|
||||
}
|
||||
#elif defined(HAVE_LIBSECRET)
|
||||
{
|
||||
static const SecretSchema schema = {
|
||||
"net.zoite.ZoiteChat.Network", SECRET_SCHEMA_NONE,
|
||||
{
|
||||
{ "network", SECRET_SCHEMA_ATTRIBUTE_STRING },
|
||||
{ NULL, 0 },
|
||||
}
|
||||
};
|
||||
gboolean ok = secret_password_store_sync (&schema, SECRET_COLLECTION_DEFAULT,
|
||||
"ZoiteChat network password", password, NULL, NULL, "network", target, NULL);
|
||||
g_free (target);
|
||||
return ok;
|
||||
}
|
||||
#else
|
||||
g_free (target);
|
||||
return FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
int secretstore_delete_network_password (const char *network_name)
|
||||
{
|
||||
char *target;
|
||||
target = secretstore_target (network_name);
|
||||
#ifdef WIN32
|
||||
{
|
||||
gboolean ok = CredDeleteA (target, CRED_TYPE_GENERIC, 0);
|
||||
g_free (target);
|
||||
return ok;
|
||||
}
|
||||
#elif defined(HAVE_LIBSECRET)
|
||||
{
|
||||
static const SecretSchema schema = {
|
||||
"net.zoite.ZoiteChat.Network", SECRET_SCHEMA_NONE,
|
||||
{
|
||||
{ "network", SECRET_SCHEMA_ATTRIBUTE_STRING },
|
||||
{ NULL, 0 },
|
||||
}
|
||||
};
|
||||
gboolean ok = secret_password_clear_sync (&schema, NULL, NULL, "network", target, NULL);
|
||||
g_free (target);
|
||||
return ok;
|
||||
}
|
||||
#else
|
||||
g_free (target);
|
||||
return FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
int secretstore_require_unlock (const char *network_name)
|
||||
{
|
||||
#ifdef WIN32
|
||||
return TRUE;
|
||||
#elif defined(HAVE_LIBSECRET)
|
||||
{
|
||||
static const SecretSchema schema = {
|
||||
"net.zoite.ZoiteChat.Network", SECRET_SCHEMA_NONE,
|
||||
{
|
||||
{ "network", SECRET_SCHEMA_ATTRIBUTE_STRING },
|
||||
{ NULL, 0 },
|
||||
}
|
||||
};
|
||||
char *target;
|
||||
char *password;
|
||||
GError *error = NULL;
|
||||
target = secretstore_target (network_name);
|
||||
password = secret_password_lookup_sync (&schema, NULL, &error, "network", target, NULL);
|
||||
g_free (target);
|
||||
if (password)
|
||||
{
|
||||
memset (password, 0, strlen (password));
|
||||
g_free (password);
|
||||
return TRUE;
|
||||
}
|
||||
if (error)
|
||||
{
|
||||
g_error_free (error);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
return TRUE;
|
||||
#endif
|
||||
}
|
||||
10
src/common/secretstore.h
Normal file
10
src/common/secretstore.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef ZOITECHAT_SECRETSTORE_H
|
||||
#define ZOITECHAT_SECRETSTORE_H
|
||||
|
||||
char *secretstore_get_network_password (const char *network_name);
|
||||
int secretstore_set_network_password (const char *network_name, const char *password);
|
||||
int secretstore_delete_network_password (const char *network_name);
|
||||
int secretstore_is_keyring_available (void);
|
||||
int secretstore_require_unlock (const char *network_name);
|
||||
|
||||
#endif
|
||||
@@ -109,6 +109,58 @@ static void server_disconnect (session * sess, int sendquit, int err);
|
||||
static int server_cleanup (server * serv);
|
||||
static void server_connect (server *serv, char *hostname, int port, int no_login);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
server *serv;
|
||||
char hostname[128];
|
||||
int port;
|
||||
} reconnect_password_prompt;
|
||||
|
||||
static gboolean
|
||||
server_login_uses_password_sasl (int logintype)
|
||||
{
|
||||
return logintype == LOGIN_SASL ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_1 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_256 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_512;
|
||||
}
|
||||
|
||||
static void
|
||||
server_reconnect_password_cb (int cancel, char *text, void *userdata)
|
||||
{
|
||||
reconnect_password_prompt *prompt = userdata;
|
||||
|
||||
if (!cancel && is_server (prompt->serv) && !prompt->serv->connected && !prompt->serv->connecting && prompt->serv->server_session)
|
||||
{
|
||||
prompt->serv->password[0] = 0;
|
||||
if (text && *text)
|
||||
safe_strcpy (prompt->serv->password, text, sizeof (prompt->serv->password));
|
||||
server_connect (prompt->serv, prompt->hostname, prompt->port, FALSE);
|
||||
}
|
||||
|
||||
g_free (prompt);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
server_prompt_reconnect_password (server *serv)
|
||||
{
|
||||
reconnect_password_prompt *prompt;
|
||||
char *msg;
|
||||
ircnet *net = serv->network;
|
||||
|
||||
if (!net || !(net->flags & FLAG_PROMPT_PASSWORD) || !server_login_uses_password_sasl (serv->loginmethod))
|
||||
return FALSE;
|
||||
|
||||
prompt = g_new0 (reconnect_password_prompt, 1);
|
||||
prompt->serv = serv;
|
||||
safe_strcpy (prompt->hostname, serv->hostname, sizeof (prompt->hostname));
|
||||
prompt->port = serv->port;
|
||||
msg = g_strdup_printf (_("Enter SASL password for %s:"), net->name ? net->name : _("this network"));
|
||||
fe_get_password (msg, server_reconnect_password_cb, prompt);
|
||||
g_free (msg);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
write_error (char *message, GError **error)
|
||||
{
|
||||
@@ -522,7 +574,7 @@ ssl_cb_verify (int ok, X509_STORE_CTX * ctx)
|
||||
X509 *current_cert = X509_STORE_CTX_get_current_cert (ctx);
|
||||
|
||||
if (!current_cert)
|
||||
return TRUE;
|
||||
return ok;
|
||||
|
||||
X509_NAME_oneline (X509_get_subject_name (current_cert),
|
||||
subject, sizeof (subject));
|
||||
@@ -534,13 +586,24 @@ ssl_cb_verify (int ok, X509_STORE_CTX * ctx)
|
||||
g_snprintf (buf, sizeof (buf), "* Issuer: %s", issuer);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, g_sess, buf, NULL, NULL, NULL, 0);
|
||||
|
||||
return TRUE;
|
||||
if (!ok)
|
||||
{
|
||||
int err = X509_STORE_CTX_get_error (ctx);
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: %s (%d)",
|
||||
X509_verify_cert_error_string (err), err);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, g_sess, buf, NULL, NULL, NULL, 0);
|
||||
|
||||
if (g_sess && g_sess->server->accept_invalid_cert)
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int
|
||||
ssl_do_connect (server * serv)
|
||||
{
|
||||
char buf[256]; // ERR_error_string() MUST have this size
|
||||
char buf[256];
|
||||
|
||||
g_sess = serv->server_session;
|
||||
|
||||
@@ -559,9 +622,10 @@ ssl_do_connect (server * serv)
|
||||
if (SSL_connect (serv->ssl) <= 0)
|
||||
{
|
||||
char err_buf[128];
|
||||
int err;
|
||||
int err, ssl_err;
|
||||
|
||||
g_sess = NULL;
|
||||
ssl_err = SSL_get_error (serv->ssl, -1);
|
||||
if ((err = ERR_get_error ()) > 0)
|
||||
{
|
||||
ERR_error_string (err, err_buf);
|
||||
@@ -571,6 +635,8 @@ ssl_do_connect (server * serv)
|
||||
|
||||
if (ERR_GET_REASON (err) == SSL_R_WRONG_VERSION_NUMBER)
|
||||
PrintText (serv->server_session, _("Are you sure this is a SSL capable server and port?\n"));
|
||||
else if (ssl_err == SSL_ERROR_SSL)
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, "* TLS handshake rejected by protocol/certificate/cipher policy", NULL, NULL, NULL, 0);
|
||||
|
||||
server_cleanup (serv);
|
||||
|
||||
@@ -649,30 +715,18 @@ ssl_do_connect (server * serv)
|
||||
int hostname_err;
|
||||
if ((hostname_err = _SSL_check_hostname(cert, serv->hostname)) != 0)
|
||||
{
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: Failed to validate hostname? (%d)%s",
|
||||
hostname_err, serv->accept_invalid_cert ? " -- Ignored" : "");
|
||||
if (serv->accept_invalid_cert)
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, buf, NULL, NULL, NULL, 0);
|
||||
else
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: Failed to validate hostname (%d)",
|
||||
hostname_err);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, buf, NULL, NULL, NULL, 0);
|
||||
if (!serv->accept_invalid_cert)
|
||||
goto conn_fail;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
|
||||
case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
|
||||
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
|
||||
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
|
||||
case X509_V_ERR_CERT_HAS_EXPIRED:
|
||||
if (serv->accept_invalid_cert)
|
||||
{
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: %s.? (%d) -- Ignored",
|
||||
X509_verify_cert_error_string (verify_error),
|
||||
verify_error);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, buf, NULL, NULL,
|
||||
NULL, 0);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (serv->accept_invalid_cert)
|
||||
break;
|
||||
|
||||
g_snprintf (buf, sizeof (buf), "%s.? (%d)",
|
||||
X509_verify_cert_error_string (verify_error),
|
||||
verify_error);
|
||||
@@ -732,7 +786,8 @@ timeout_auto_reconnect (server *serv)
|
||||
serv->recondelay_tag = 0;
|
||||
if (!serv->connected && !serv->connecting && serv->server_session)
|
||||
{
|
||||
server_connect (serv, serv->hostname, serv->port, FALSE);
|
||||
if (!server_prompt_reconnect_password (serv))
|
||||
server_connect (serv, serv->hostname, serv->port, FALSE);
|
||||
}
|
||||
}
|
||||
return 0; /* returning 0 should remove the timeout handler */
|
||||
@@ -881,7 +936,7 @@ server_read_child (GIOChannel *source, GIOCondition condition, server *serv)
|
||||
if (prefs.hex_net_auto_reconnectonfail)
|
||||
auto_reconnect (serv, FALSE, -1);
|
||||
break;
|
||||
case '3': /* gethostbyname finished */
|
||||
case '3': /* resolver finished */
|
||||
waitline2 (source, host, sizeof host);
|
||||
waitline2 (source, ip, sizeof ip);
|
||||
waitline2 (source, outbuf, sizeof outbuf);
|
||||
@@ -930,9 +985,9 @@ server_read_child (GIOChannel *source, GIOCondition condition, server *serv)
|
||||
break;
|
||||
case '5': /* prefs ip discovered */
|
||||
waitline2 (source, tbuf, sizeof tbuf);
|
||||
prefs.local_ip = inet_addr (tbuf);
|
||||
net_parse_ipv4 (tbuf, &prefs.local_ip);
|
||||
break;
|
||||
case '7': /* gethostbyname (prefs.hex_net_bind_host) failed */
|
||||
case '7': /* prefs.hex_net_bind_host resolve failed */
|
||||
sprintf (outbuf,
|
||||
_("Cannot resolve hostname %s\nCheck your IP Settings!\n"),
|
||||
prefs.hex_net_bind_host);
|
||||
@@ -1104,7 +1159,7 @@ traverse_socks (int print_fd, int sok, char *serverAddr, int port)
|
||||
sc.version = 4;
|
||||
sc.type = 1;
|
||||
sc.port = htons (port);
|
||||
sc.address = inet_addr (serverAddr);
|
||||
net_parse_ipv4 (serverAddr, &sc.address);
|
||||
g_strlcpy (sc.username, prefs.hex_irc_user_name, sizeof (sc.username));
|
||||
|
||||
send (sok, (char *) &sc, 8 + strlen (sc.username) + 1, 0);
|
||||
@@ -1819,6 +1874,7 @@ void
|
||||
server_set_defaults (server *serv)
|
||||
{
|
||||
g_free (serv->chantypes);
|
||||
g_clear_pointer (&serv->clienttagdeny, g_free);
|
||||
g_free (serv->chanmodes);
|
||||
g_free (serv->nick_prefixes);
|
||||
g_free (serv->nick_modes);
|
||||
@@ -1853,6 +1909,8 @@ server_set_defaults (server *serv)
|
||||
serv->have_extjoin = FALSE;
|
||||
serv->have_account_tag = FALSE;
|
||||
serv->have_server_time = FALSE;
|
||||
serv->have_message_tags = FALSE;
|
||||
serv->have_echo_message = FALSE;
|
||||
serv->have_sasl = FALSE;
|
||||
serv->have_except = FALSE;
|
||||
serv->have_invite = FALSE;
|
||||
@@ -1983,6 +2041,7 @@ server_free (server *serv)
|
||||
g_free (serv->nick_prefixes);
|
||||
g_free (serv->chanmodes);
|
||||
g_free (serv->chantypes);
|
||||
g_free (serv->clienttagdeny);
|
||||
g_free (serv->bad_nick_prefixes);
|
||||
g_free (serv->last_away_reason);
|
||||
g_free (serv->encoding);
|
||||
|
||||
@@ -33,9 +33,152 @@
|
||||
#include "text.h"
|
||||
#include "util.h" /* token_foreach */
|
||||
#include "zoitechatc.h"
|
||||
#include "secretstore.h"
|
||||
|
||||
#include "servlist.h"
|
||||
|
||||
#ifdef USE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/rand.h>
|
||||
#endif
|
||||
|
||||
char *
|
||||
servlist_password_encrypt_for_storage (const char *pass)
|
||||
{
|
||||
gchar *material;
|
||||
unsigned char salt[16];
|
||||
unsigned char iv[16];
|
||||
unsigned char key[32];
|
||||
unsigned char *ciphertext;
|
||||
int outlen1;
|
||||
int outlen2;
|
||||
int inlen;
|
||||
int cipherlen;
|
||||
EVP_CIPHER_CTX *ctx;
|
||||
char *b64;
|
||||
char *ret;
|
||||
if (!pass || !*pass)
|
||||
return NULL;
|
||||
#ifdef USE_OPENSSL
|
||||
if (RAND_bytes (salt, sizeof (salt)) != 1 || RAND_bytes (iv, sizeof (iv)) != 1)
|
||||
return NULL;
|
||||
material = g_strdup_printf ("%s|%s", g_get_user_name (), get_xdir ());
|
||||
if (!PKCS5_PBKDF2_HMAC (material, -1, salt, sizeof (salt), 300000, EVP_sha256 (), sizeof (key), key))
|
||||
{
|
||||
g_free (material);
|
||||
return NULL;
|
||||
}
|
||||
g_free (material);
|
||||
inlen = (int) strlen (pass);
|
||||
ciphertext = g_malloc (inlen + EVP_MAX_BLOCK_LENGTH);
|
||||
ctx = EVP_CIPHER_CTX_new ();
|
||||
if (!ctx)
|
||||
{
|
||||
memset (key, 0, sizeof (key));
|
||||
g_free (ciphertext);
|
||||
return NULL;
|
||||
}
|
||||
if (EVP_EncryptInit_ex (ctx, EVP_aes_256_cbc (), NULL, key, iv) != 1 ||
|
||||
EVP_EncryptUpdate (ctx, ciphertext, &outlen1, (const unsigned char *) pass, inlen) != 1 ||
|
||||
EVP_EncryptFinal_ex (ctx, ciphertext + outlen1, &outlen2) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free (ctx);
|
||||
memset (key, 0, sizeof (key));
|
||||
g_free (ciphertext);
|
||||
return NULL;
|
||||
}
|
||||
EVP_CIPHER_CTX_free (ctx);
|
||||
cipherlen = outlen1 + outlen2;
|
||||
{
|
||||
gsize payload_len = sizeof (salt) + sizeof (iv) + (gsize) cipherlen;
|
||||
unsigned char *payload = g_malloc (payload_len);
|
||||
memcpy (payload, salt, sizeof (salt));
|
||||
memcpy (payload + sizeof (salt), iv, sizeof (iv));
|
||||
memcpy (payload + sizeof (salt) + sizeof (iv), ciphertext, cipherlen);
|
||||
b64 = g_base64_encode (payload, payload_len);
|
||||
memset (payload, 0, payload_len);
|
||||
g_free (payload);
|
||||
}
|
||||
memset (key, 0, sizeof (key));
|
||||
memset (ciphertext, 0, inlen + EVP_MAX_BLOCK_LENGTH);
|
||||
g_free (ciphertext);
|
||||
#else
|
||||
b64 = g_base64_encode ((const guchar *) pass, strlen (pass));
|
||||
#endif
|
||||
ret = g_strdup_printf ("enc:%s", b64);
|
||||
g_free (b64);
|
||||
return ret;
|
||||
}
|
||||
|
||||
gboolean
|
||||
servlist_password_is_encrypted (const char *pass)
|
||||
{
|
||||
return pass && g_str_has_prefix (pass, "enc:");
|
||||
}
|
||||
|
||||
char *
|
||||
servlist_password_decrypt_for_storage (const char *enc)
|
||||
{
|
||||
guchar *raw;
|
||||
gsize len;
|
||||
char *ret;
|
||||
if (!enc || !*enc)
|
||||
return NULL;
|
||||
if (!g_str_has_prefix (enc, "enc:"))
|
||||
return g_strdup (enc);
|
||||
raw = g_base64_decode (enc + 4, &len);
|
||||
#ifdef USE_OPENSSL
|
||||
if (len <= 32)
|
||||
{
|
||||
g_free (raw);
|
||||
return NULL;
|
||||
}
|
||||
{
|
||||
unsigned char *salt = raw;
|
||||
unsigned char *iv = raw + 16;
|
||||
unsigned char *ciphertext = raw + 32;
|
||||
int cipherlen = (int) (len - 32);
|
||||
unsigned char key[32];
|
||||
gchar *material = g_strdup_printf ("%s|%s", g_get_user_name (), get_xdir ());
|
||||
unsigned char *plaintext = g_malloc ((gsize) cipherlen + 1);
|
||||
int outlen1;
|
||||
int outlen2;
|
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new ();
|
||||
if (!ctx || !PKCS5_PBKDF2_HMAC (material, -1, salt, 16, 300000, EVP_sha256 (), sizeof (key), key))
|
||||
{
|
||||
g_free (material);
|
||||
if (ctx)
|
||||
EVP_CIPHER_CTX_free (ctx);
|
||||
g_free (plaintext);
|
||||
g_free (raw);
|
||||
return NULL;
|
||||
}
|
||||
g_free (material);
|
||||
if (EVP_DecryptInit_ex (ctx, EVP_aes_256_cbc (), NULL, key, iv) != 1 ||
|
||||
EVP_DecryptUpdate (ctx, plaintext, &outlen1, ciphertext, cipherlen) != 1 ||
|
||||
EVP_DecryptFinal_ex (ctx, plaintext + outlen1, &outlen2) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free (ctx);
|
||||
memset (key, 0, sizeof (key));
|
||||
memset (plaintext, 0, (gsize) cipherlen + 1);
|
||||
g_free (plaintext);
|
||||
g_free (raw);
|
||||
return NULL;
|
||||
}
|
||||
EVP_CIPHER_CTX_free (ctx);
|
||||
memset (key, 0, sizeof (key));
|
||||
plaintext[outlen1 + outlen2] = 0;
|
||||
ret = g_strdup ((const char *) plaintext);
|
||||
memset (plaintext, 0, (gsize) cipherlen + 1);
|
||||
g_free (plaintext);
|
||||
}
|
||||
#else
|
||||
ret = g_strndup ((const char *) raw, len);
|
||||
#endif
|
||||
g_free (raw);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
struct defaultserver
|
||||
{
|
||||
@@ -296,8 +439,99 @@ servlist_favchan_copy (favchannel *fav)
|
||||
return newfav;
|
||||
}
|
||||
|
||||
static void servlist_connect_with_password (session *sess, ircnet *net, gboolean join, const char *password);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
session *sess;
|
||||
ircnet *net;
|
||||
gboolean join;
|
||||
} servlist_password_prompt;
|
||||
|
||||
static gboolean
|
||||
servlist_login_uses_password_sasl (int logintype)
|
||||
{
|
||||
return logintype == LOGIN_SASL ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_1 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_256 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_512;
|
||||
}
|
||||
|
||||
static void servlist_prompt_sasl_password (servlist_password_prompt *prompt);
|
||||
|
||||
static void
|
||||
servlist_connect_without_password_cb (int value, void *userdata)
|
||||
{
|
||||
servlist_password_prompt *prompt = userdata;
|
||||
|
||||
if (value)
|
||||
{
|
||||
servlist_connect_with_password (prompt->sess, prompt->net, prompt->join, NULL);
|
||||
g_free (prompt);
|
||||
}
|
||||
else
|
||||
{
|
||||
servlist_prompt_sasl_password (prompt);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
servlist_connect_password_cb (int cancel, char *text, void *userdata)
|
||||
{
|
||||
servlist_password_prompt *prompt = userdata;
|
||||
char *msg;
|
||||
|
||||
if (cancel)
|
||||
{
|
||||
fe_serverlist_open (prompt->sess);
|
||||
g_free (prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text && *text)
|
||||
{
|
||||
servlist_connect_with_password (prompt->sess, prompt->net, prompt->join, text);
|
||||
g_free (prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
msg = g_strdup_printf (_("Continue connecting to %s without a SASL password?"), prompt->net->name ? prompt->net->name : _("this network"));
|
||||
fe_get_bool (_("No SASL password"), msg, servlist_connect_without_password_cb, prompt);
|
||||
g_free (msg);
|
||||
}
|
||||
|
||||
static void
|
||||
servlist_prompt_sasl_password (servlist_password_prompt *prompt)
|
||||
{
|
||||
char *msg;
|
||||
|
||||
msg = g_strdup_printf (_("Enter SASL password for %s:"), prompt->net->name ? prompt->net->name : _("this network"));
|
||||
fe_get_password (msg, servlist_connect_password_cb, prompt);
|
||||
g_free (msg);
|
||||
}
|
||||
|
||||
void
|
||||
servlist_connect (session *sess, ircnet *net, gboolean join)
|
||||
{
|
||||
int logintype;
|
||||
servlist_password_prompt *prompt;
|
||||
|
||||
logintype = net && net->logintype ? net->logintype : LOGIN_DEFAULT_REAL;
|
||||
if (net && (net->flags & FLAG_PROMPT_PASSWORD) && servlist_login_uses_password_sasl (logintype))
|
||||
{
|
||||
prompt = g_new0 (servlist_password_prompt, 1);
|
||||
prompt->sess = sess;
|
||||
prompt->net = net;
|
||||
prompt->join = join;
|
||||
servlist_prompt_sasl_password (prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
servlist_connect_with_password (sess, net, join, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
servlist_connect_with_password (session *sess, ircnet *net, gboolean join, const char *password)
|
||||
{
|
||||
ircserver *ircserv;
|
||||
GSList *list;
|
||||
@@ -344,10 +578,33 @@ servlist_connect (session *sess, ircnet *net, gboolean join)
|
||||
}
|
||||
|
||||
serv->password[0] = 0;
|
||||
|
||||
if (net->pass)
|
||||
if (password && *password)
|
||||
{
|
||||
safe_strcpy (serv->password, net->pass, sizeof (serv->password));
|
||||
safe_strcpy (serv->password, password, sizeof (serv->password));
|
||||
}
|
||||
else if ((net->flags & FLAG_USE_KEYRING) && net->name)
|
||||
{
|
||||
char *stored_pass = secretstore_get_network_password (net->name);
|
||||
if (stored_pass && *stored_pass)
|
||||
{
|
||||
safe_strcpy (serv->password, stored_pass, sizeof (serv->password));
|
||||
}
|
||||
if (stored_pass)
|
||||
{
|
||||
memset (stored_pass, 0, strlen (stored_pass));
|
||||
g_free (stored_pass);
|
||||
}
|
||||
}
|
||||
else if (net->pass)
|
||||
{
|
||||
char *plain = servlist_password_decrypt_for_storage (net->pass);
|
||||
if (plain && *plain)
|
||||
safe_strcpy (serv->password, plain, sizeof (serv->password));
|
||||
if (plain)
|
||||
{
|
||||
memset (plain, 0, strlen (plain));
|
||||
g_free (plain);
|
||||
}
|
||||
}
|
||||
|
||||
if (net->flags & FLAG_USE_GLOBAL)
|
||||
@@ -982,24 +1239,6 @@ servlist_load (void)
|
||||
*
|
||||
* Should be removed at some point.
|
||||
*/
|
||||
case 'A':
|
||||
if (!net->pass)
|
||||
{
|
||||
net->pass = g_strdup (buf + 2);
|
||||
if (!net->logintype)
|
||||
{
|
||||
net->logintype = LOGIN_SASL;
|
||||
}
|
||||
}
|
||||
case 'B':
|
||||
if (!net->pass)
|
||||
{
|
||||
net->pass = g_strdup (buf + 2);
|
||||
if (!net->logintype)
|
||||
{
|
||||
net->logintype = LOGIN_NICKSERV;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (buf[0] == 'N')
|
||||
|
||||
@@ -62,7 +62,9 @@ extern GSList *network_list;
|
||||
#define FLAG_USE_PROXY 16
|
||||
#define FLAG_ALLOW_INVALID 32
|
||||
#define FLAG_FAVORITE 64
|
||||
#define FLAG_COUNT 7
|
||||
#define FLAG_USE_KEYRING 128
|
||||
#define FLAG_PROMPT_PASSWORD 256
|
||||
#define FLAG_COUNT 9
|
||||
|
||||
/* Login methods. Use server password by default - if we had a NickServ password, it'd be set to 2 already by servlist_load() */
|
||||
#define LOGIN_DEFAULT_REAL LOGIN_PASS /* this is to set the default login method for unknown servers */
|
||||
@@ -124,5 +126,8 @@ favchannel *servlist_favchan_copy (favchannel *fav);
|
||||
GSList *servlist_favchan_listadd (GSList *chanlist, char *channel, char *key);
|
||||
|
||||
gboolean joinlist_is_in_list (server *serv, char *channel);
|
||||
char *servlist_password_encrypt_for_storage (const char *pass);
|
||||
char *servlist_password_decrypt_for_storage (const char *pass);
|
||||
gboolean servlist_password_is_encrypted (const char *pass);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -86,15 +86,17 @@ _SSL_context_init (void (*info_cb_func))
|
||||
|
||||
SSLeay_add_ssl_algorithms ();
|
||||
SSL_load_error_strings ();
|
||||
ctx = SSL_CTX_new (SSLv23_client_method ());
|
||||
ctx = SSL_CTX_new (TLS_client_method ());
|
||||
|
||||
SSL_CTX_set_session_cache_mode (ctx, SSL_SESS_CACHE_BOTH);
|
||||
SSL_CTX_set_timeout (ctx, 300);
|
||||
SSL_CTX_set_options (ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3
|
||||
|SSL_OP_NO_COMPRESSION
|
||||
SSL_CTX_set_options (ctx, SSL_OP_NO_COMPRESSION
|
||||
|SSL_OP_SINGLE_DH_USE|SSL_OP_SINGLE_ECDH_USE
|
||||
|SSL_OP_NO_TICKET
|
||||
|SSL_OP_CIPHER_SERVER_PREFERENCE);
|
||||
|SSL_OP_NO_RENEGOTIATION);
|
||||
SSL_CTX_set_min_proto_version (ctx, TLS1_2_VERSION);
|
||||
SSL_CTX_set_cipher_list (ctx, "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!DSS");
|
||||
SSL_CTX_set_ciphersuites (ctx, "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256");
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x00908000L && !defined (OPENSSL_NO_COMP) /* workaround for OpenSSL 0.9.8 */
|
||||
sk_SSL_COMP_zero(SSL_COMP_get_compression_methods());
|
||||
@@ -152,7 +154,7 @@ int
|
||||
_SSL_get_cert_info (struct cert_info *cert_info, SSL * ssl)
|
||||
{
|
||||
X509 *peer_cert;
|
||||
X509_PUBKEY *key;
|
||||
const X509_PUBKEY *key;
|
||||
X509_ALGOR *algor = NULL;
|
||||
EVP_PKEY *peer_pkey;
|
||||
char notBefore[64];
|
||||
@@ -311,7 +313,7 @@ _SSL_socket (SSL_CTX *ctx, int sd)
|
||||
#else
|
||||
method = SSL_CTX_get_ssl_method (ctx);
|
||||
#endif
|
||||
if (method == SSLv23_client_method())
|
||||
if (method == TLS_client_method())
|
||||
SSL_set_connect_state (ssl);
|
||||
else
|
||||
SSL_set_accept_state(ssl);
|
||||
@@ -348,14 +350,13 @@ _SSL_close (SSL * ssl)
|
||||
{
|
||||
SSL_set_shutdown (ssl, SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN);
|
||||
SSL_free (ssl);
|
||||
#if !defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
#ifdef HAVE_ERR_REMOVE_THREAD_STATE
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10000000L && OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
/* OpenSSL handles this itself in 1.1+ and this is a no-op */
|
||||
ERR_remove_thread_state (NULL);
|
||||
#endif
|
||||
#else
|
||||
ERR_remove_state (0);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Hostname validation code based on OpenBSD's libtls. */
|
||||
@@ -506,9 +507,12 @@ _SSL_check_subject_altname (X509 *cert, const char *host)
|
||||
static int
|
||||
_SSL_check_common_name (X509 *cert, const char *host)
|
||||
{
|
||||
X509_NAME *name;
|
||||
char *common_name = NULL;
|
||||
const X509_NAME *name;
|
||||
const X509_NAME_ENTRY *entry;
|
||||
const ASN1_STRING *common_name_asn1;
|
||||
const unsigned char *common_name;
|
||||
int common_name_len;
|
||||
int common_name_index;
|
||||
int rv = -1;
|
||||
GInetAddress *addr;
|
||||
|
||||
@@ -516,16 +520,27 @@ _SSL_check_common_name (X509 *cert, const char *host)
|
||||
if (name == NULL)
|
||||
return -1;
|
||||
|
||||
common_name_len = X509_NAME_get_text_by_NID (name, NID_commonName, NULL, 0);
|
||||
if (common_name_len < 0)
|
||||
common_name_index = X509_NAME_get_index_by_NID (name, NID_commonName, -1);
|
||||
if (common_name_index < 0)
|
||||
return -1;
|
||||
|
||||
common_name = g_malloc0 (common_name_len + 1);
|
||||
entry = X509_NAME_get_entry (name, common_name_index);
|
||||
if (entry == NULL)
|
||||
return -1;
|
||||
|
||||
X509_NAME_get_text_by_NID (name, NID_commonName, common_name, common_name_len + 1);
|
||||
common_name_asn1 = X509_NAME_ENTRY_get_data (entry);
|
||||
if (common_name_asn1 == NULL)
|
||||
return -1;
|
||||
|
||||
#ifdef HAVE_ASN1_STRING_GET0_DATA
|
||||
common_name = ASN1_STRING_get0_data (common_name_asn1);
|
||||
#else
|
||||
common_name = ASN1_STRING_data (common_name_asn1);
|
||||
#endif
|
||||
common_name_len = ASN1_STRING_length (common_name_asn1);
|
||||
|
||||
/* NUL bytes in CN? */
|
||||
if (common_name_len != (int)strlen(common_name))
|
||||
if (common_name_len != (int)strlen((const char *)common_name))
|
||||
{
|
||||
g_warning ("NUL byte in Common Name field, probably a malicious certificate.\n");
|
||||
rv = -2;
|
||||
@@ -538,18 +553,17 @@ _SSL_check_common_name (X509 *cert, const char *host)
|
||||
* We don't want to attempt wildcard matching against IP
|
||||
* addresses, so perform a simple comparison here.
|
||||
*/
|
||||
if (g_strcmp0 (common_name, host) == 0)
|
||||
if (g_strcmp0 ((const char *)common_name, host) == 0)
|
||||
rv = 0;
|
||||
else
|
||||
rv = -1;
|
||||
|
||||
g_object_unref (addr);
|
||||
}
|
||||
else if (_SSL_match_hostname (common_name, host) == 0)
|
||||
else if (_SSL_match_hostname ((const char *)common_name, host) == 0)
|
||||
rv = 0;
|
||||
|
||||
out:
|
||||
g_free(common_name);
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <ctype.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
@@ -74,6 +75,18 @@ typedef struct
|
||||
static bool string_builder_init (StringBuilder *builder);
|
||||
static void string_builder_free (StringBuilder *builder);
|
||||
static bool string_builder_append (StringBuilder *builder, const char *text);
|
||||
static int size_to_int (size_t value);
|
||||
|
||||
static int
|
||||
size_to_int (size_t value)
|
||||
{
|
||||
if (value > (size_t) INT_MAX)
|
||||
{
|
||||
return INT_MAX;
|
||||
}
|
||||
|
||||
return (int) value;
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_get_cpu (void)
|
||||
@@ -511,6 +524,7 @@ static char *read_hdd_info (IWbemClassObject *object)
|
||||
static char *bstr_to_utf8 (BSTR bstr)
|
||||
{
|
||||
int utf8_len;
|
||||
int wide_len;
|
||||
char *utf8;
|
||||
|
||||
if (bstr == NULL)
|
||||
@@ -518,7 +532,8 @@ static char *bstr_to_utf8 (BSTR bstr)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
utf8_len = WideCharToMultiByte (CP_UTF8, 0, bstr, SysStringLen (bstr), NULL, 0, NULL, NULL);
|
||||
wide_len = size_to_int ((size_t) SysStringLen (bstr));
|
||||
utf8_len = WideCharToMultiByte (CP_UTF8, 0, bstr, wide_len, NULL, 0, NULL, NULL);
|
||||
if (utf8_len <= 0)
|
||||
{
|
||||
return NULL;
|
||||
@@ -530,7 +545,7 @@ static char *bstr_to_utf8 (BSTR bstr)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (WideCharToMultiByte (CP_UTF8, 0, bstr, SysStringLen (bstr), utf8, utf8_len, NULL, NULL) <= 0)
|
||||
if (WideCharToMultiByte (CP_UTF8, 0, bstr, wide_len, utf8, utf8_len, NULL, NULL) <= 0)
|
||||
{
|
||||
free (utf8);
|
||||
return NULL;
|
||||
|
||||
240
src/common/tests/test-emoji-data.c
Normal file
240
src/common/tests/test-emoji-data.c
Normal file
@@ -0,0 +1,240 @@
|
||||
/* ZoiteChat
|
||||
* Copyright (C) 2026 deepend-tildeclub.
|
||||
*
|
||||
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include "../emoji-data.h"
|
||||
|
||||
#define GRINNING_FACE "\360\237\230\200" /* 😀 */
|
||||
#define DUCK "\360\237\246\206" /* 🦆 */
|
||||
#define THUMBS_UP "\360\237\221\215" /* 👍 */
|
||||
#define WAVE_DARK "\360\237\221\213\360\237\217\277" /* 👋🏿 */
|
||||
#define MEDIUM_TONE_MODIFIER "\360\237\217\275" /* U+1F3FD */
|
||||
#define KEYCAP_HASH "#\357\270\217\342\203\243" /* #️⃣ */
|
||||
|
||||
static const EmojiEntry *
|
||||
find_by_name (const char *name)
|
||||
{
|
||||
gsize i;
|
||||
|
||||
for (i = 0; i < emoji_data_count (); i++)
|
||||
{
|
||||
const EmojiEntry *entry = emoji_data_entry (i);
|
||||
|
||||
if (!strcmp (entry->name, name))
|
||||
return entry;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
test_catalog_integrity (void)
|
||||
{
|
||||
gsize i;
|
||||
|
||||
g_assert_cmpuint (emoji_data_count (), >=, 1800);
|
||||
|
||||
for (i = 0; i < emoji_data_count (); i++)
|
||||
{
|
||||
const EmojiEntry *entry = emoji_data_entry (i);
|
||||
|
||||
g_assert_nonnull (entry->sequence);
|
||||
g_assert_true (*entry->sequence != '\0');
|
||||
g_assert_true (g_utf8_validate (entry->sequence, -1, NULL));
|
||||
g_assert_nonnull (entry->name);
|
||||
g_assert_true (*entry->name != '\0');
|
||||
g_assert_true (g_utf8_validate (entry->name, -1, NULL));
|
||||
g_assert_nonnull (entry->search);
|
||||
g_assert_true (*entry->search != '\0');
|
||||
g_assert_nonnull (entry->aliases);
|
||||
g_assert_cmpuint (entry->group, <, EMOJI_GROUP_COUNT);
|
||||
g_assert_cmpint (entry->tone_set, >=, -1);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
test_cldr_order (void)
|
||||
{
|
||||
const EmojiEntry *first = emoji_data_entry (0);
|
||||
|
||||
g_assert_cmpstr (first->sequence, ==, GRINNING_FACE);
|
||||
g_assert_cmpuint (first->group, ==, EMOJI_GROUP_SMILEYS_EMOTION);
|
||||
}
|
||||
|
||||
static void
|
||||
test_groups (void)
|
||||
{
|
||||
int group;
|
||||
gboolean seen[EMOJI_GROUP_COUNT] = { FALSE };
|
||||
gsize i;
|
||||
|
||||
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
|
||||
{
|
||||
g_assert_true (*emoji_data_group_display_name (group) != '\0');
|
||||
g_assert_true (*emoji_data_group_icon_sequence (group) != '\0');
|
||||
}
|
||||
|
||||
for (i = 0; i < emoji_data_count (); i++)
|
||||
seen[emoji_data_entry (i)->group] = TRUE;
|
||||
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
|
||||
g_assert_true (seen[group]);
|
||||
}
|
||||
|
||||
static void
|
||||
test_search_by_keyword (void)
|
||||
{
|
||||
const EmojiEntry *duck = find_by_name ("duck");
|
||||
char **tokens = emoji_search_tokenize ("Duck");
|
||||
|
||||
g_assert_nonnull (duck);
|
||||
g_assert_cmpstr (duck->sequence, ==, DUCK);
|
||||
g_assert_nonnull (tokens);
|
||||
g_assert_true (emoji_entry_matches (duck, tokens));
|
||||
g_strfreev (tokens);
|
||||
|
||||
tokens = emoji_search_tokenize ("bird");
|
||||
g_assert_true (emoji_entry_matches (duck, tokens));
|
||||
g_strfreev (tokens);
|
||||
|
||||
tokens = emoji_search_tokenize ("no-such-emoji-keyword");
|
||||
g_assert_false (emoji_entry_matches (duck, tokens));
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
static void
|
||||
test_search_by_alias (void)
|
||||
{
|
||||
const EmojiEntry *thumbs = find_by_name ("thumbs up");
|
||||
char **tokens;
|
||||
|
||||
g_assert_nonnull (thumbs);
|
||||
g_assert_nonnull (strstr (thumbs->aliases, "thumbsup"));
|
||||
|
||||
tokens = emoji_search_tokenize (":thumbsup:");
|
||||
g_assert_nonnull (tokens);
|
||||
g_assert_true (emoji_entry_matches (thumbs, tokens));
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
static void
|
||||
test_multi_token_search (void)
|
||||
{
|
||||
const EmojiEntry *flag = find_by_name ("flag: Canada");
|
||||
char **tokens = emoji_search_tokenize ("flag canada");
|
||||
|
||||
g_assert_nonnull (flag);
|
||||
g_assert_cmpuint (flag->group, ==, EMOJI_GROUP_FLAGS);
|
||||
g_assert_nonnull (tokens);
|
||||
g_assert_true (emoji_entry_matches (flag, tokens));
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
static void
|
||||
test_skin_tones (void)
|
||||
{
|
||||
const EmojiEntry *thumbs = find_by_name ("thumbs up");
|
||||
const EmojiEntry *duck = find_by_name ("duck");
|
||||
const char *toned;
|
||||
|
||||
g_assert_nonnull (thumbs);
|
||||
g_assert_cmpint (thumbs->tone_set, >=, 0);
|
||||
|
||||
toned = emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_MEDIUM);
|
||||
g_assert_cmpstr (toned, !=, thumbs->sequence);
|
||||
g_assert_nonnull (strstr (toned, MEDIUM_TONE_MODIFIER));
|
||||
g_assert_cmpstr (emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_NONE),
|
||||
==, thumbs->sequence);
|
||||
|
||||
g_assert_nonnull (duck);
|
||||
g_assert_cmpint (duck->tone_set, ==, -1);
|
||||
g_assert_cmpstr (emoji_entry_sequence_for_tone (duck, EMOJI_TONE_DARK),
|
||||
==, duck->sequence);
|
||||
}
|
||||
|
||||
static void
|
||||
test_find_by_sequence (void)
|
||||
{
|
||||
EmojiTone tone;
|
||||
const EmojiEntry *entry;
|
||||
|
||||
entry = emoji_data_find_by_sequence (GRINNING_FACE, &tone);
|
||||
g_assert_nonnull (entry);
|
||||
g_assert_cmpstr (entry->name, ==, "grinning face");
|
||||
g_assert_cmpuint (tone, ==, EMOJI_TONE_NONE);
|
||||
|
||||
entry = emoji_data_find_by_sequence (WAVE_DARK, &tone);
|
||||
g_assert_nonnull (entry);
|
||||
g_assert_cmpstr (entry->name, ==, "waving hand");
|
||||
g_assert_cmpuint (tone, ==, EMOJI_TONE_DARK);
|
||||
|
||||
g_assert_null (emoji_data_find_by_sequence ("not an emoji", &tone));
|
||||
g_assert_null (emoji_data_find_by_sequence ("", NULL));
|
||||
g_assert_null (emoji_data_find_by_sequence (NULL, NULL));
|
||||
}
|
||||
|
||||
static void
|
||||
test_grapheme_sequences_intact (void)
|
||||
{
|
||||
const EmojiEntry *keycap = find_by_name ("keycap: #");
|
||||
const EmojiEntry *family = find_by_name ("family: man, woman, boy");
|
||||
|
||||
g_assert_nonnull (keycap);
|
||||
g_assert_cmpstr (keycap->sequence, ==, KEYCAP_HASH);
|
||||
|
||||
g_assert_nonnull (family);
|
||||
g_assert_nonnull (strstr (family->sequence, "\342\200\215"));
|
||||
}
|
||||
|
||||
static void
|
||||
test_tokenize_edge_cases (void)
|
||||
{
|
||||
g_assert_null (emoji_search_tokenize (NULL));
|
||||
g_assert_null (emoji_search_tokenize (""));
|
||||
g_assert_null (emoji_search_tokenize (" "));
|
||||
g_assert_null (emoji_search_tokenize (":::"));
|
||||
}
|
||||
|
||||
static void
|
||||
test_versions (void)
|
||||
{
|
||||
g_assert_true (*emoji_data_unicode_version () != '\0');
|
||||
g_assert_true (*emoji_data_cldr_version () != '\0');
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
g_test_add_func ("/emoji-data/catalog-integrity", test_catalog_integrity);
|
||||
g_test_add_func ("/emoji-data/cldr-order", test_cldr_order);
|
||||
g_test_add_func ("/emoji-data/groups", test_groups);
|
||||
g_test_add_func ("/emoji-data/search-by-keyword", test_search_by_keyword);
|
||||
g_test_add_func ("/emoji-data/search-by-alias", test_search_by_alias);
|
||||
g_test_add_func ("/emoji-data/multi-token-search", test_multi_token_search);
|
||||
g_test_add_func ("/emoji-data/skin-tones", test_skin_tones);
|
||||
g_test_add_func ("/emoji-data/find-by-sequence", test_find_by_sequence);
|
||||
g_test_add_func ("/emoji-data/grapheme-sequences-intact",
|
||||
test_grapheme_sequences_intact);
|
||||
g_test_add_func ("/emoji-data/tokenize-edge-cases",
|
||||
test_tokenize_edge_cases);
|
||||
g_test_add_func ("/emoji-data/versions", test_versions);
|
||||
return g_test_run ();
|
||||
}
|
||||
@@ -381,9 +381,10 @@ test_archive_without_theme_reports_css_error (void)
|
||||
}
|
||||
archive_path = g_build_filename (tmp_root, "invalid-theme.zip", NULL);
|
||||
|
||||
command = g_strdup_printf ("cd %s && zip -qr %s .", archive_root, archive_path);
|
||||
command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", archive_root, archive_path);
|
||||
g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
|
||||
g_free (command);
|
||||
g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR));
|
||||
|
||||
g_assert_false (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, &error));
|
||||
g_assert_null (imported_id);
|
||||
@@ -397,25 +398,31 @@ test_archive_without_theme_reports_css_error (void)
|
||||
}
|
||||
|
||||
static void
|
||||
test_import_rejects_theme_missing_index_theme (void)
|
||||
test_import_accepts_theme_missing_index_theme (void)
|
||||
{
|
||||
char *tmp_root;
|
||||
char *src_root;
|
||||
char *theme_root;
|
||||
char *imported_id = NULL;
|
||||
GError *error = NULL;
|
||||
ZoitechatGtk3Theme *found;
|
||||
|
||||
setup_test_xdir (&tmp_root);
|
||||
src_root = g_build_filename (tmp_root, "src", NULL);
|
||||
g_mkdir_with_parents (src_root, 0700);
|
||||
theme_root = make_theme_dir (src_root, "NoIndex", FALSE, FALSE);
|
||||
|
||||
g_assert_false (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
|
||||
g_assert_null (imported_id);
|
||||
g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
|
||||
g_assert_nonnull (g_strstr_len (error->message, -1, "missing required index.theme"));
|
||||
g_assert_nonnull (g_strstr_len (error->message, -1, "NoIndex"));
|
||||
g_error_free (error);
|
||||
/* index.theme is optional for GTK3 CSS themes: gtk-3.x/gtk.css alone
|
||||
* makes a loadable theme. */
|
||||
g_assert_true (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
|
||||
g_assert_no_error (error);
|
||||
g_assert_nonnull (imported_id);
|
||||
|
||||
found = zoitechat_gtk3_theme_find_by_id (imported_id);
|
||||
g_assert_nonnull (found);
|
||||
g_assert_true (g_str_has_suffix (found->path, "NoIndex"));
|
||||
zoitechat_gtk3_theme_free (found);
|
||||
g_free (imported_id);
|
||||
|
||||
g_free (theme_root);
|
||||
g_free (src_root);
|
||||
@@ -663,9 +670,10 @@ test_zip_import_nested_root (void)
|
||||
theme = make_theme_dir (nested, "Juno-ocean", TRUE, FALSE);
|
||||
archive_path = g_build_filename (tmp_root, "themes.zip", NULL);
|
||||
|
||||
command = g_strdup_printf ("cd %s && zip -qr %s .", zip_root, archive_path);
|
||||
command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", zip_root, archive_path);
|
||||
g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
|
||||
g_free (command);
|
||||
g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR));
|
||||
|
||||
g_assert_true (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, NULL));
|
||||
found = zoitechat_gtk3_theme_find_by_id (imported_id);
|
||||
@@ -697,7 +705,7 @@ main (int argc, char **argv)
|
||||
g_test_add_func ("/gtk3-theme-service/zip-import-nested-root", test_zip_import_nested_root);
|
||||
g_test_add_func ("/gtk3-theme-service/invalid-archive-extract-error", test_invalid_archive_reports_extract_error);
|
||||
g_test_add_func ("/gtk3-theme-service/archive-without-theme-css-error", test_archive_without_theme_reports_css_error);
|
||||
g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_rejects_theme_missing_index_theme);
|
||||
g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_accepts_theme_missing_index_theme);
|
||||
g_test_add_func ("/gtk3-theme-service/import-missing-desktop-entry", test_import_rejects_index_without_desktop_entry);
|
||||
g_test_add_func ("/gtk3-theme-service/import-unresolved-inherits", test_import_rejects_unresolved_inherits);
|
||||
return g_test_run ();
|
||||
|
||||
@@ -1695,6 +1695,97 @@ pevent_load (char *filename)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static gboolean
|
||||
text_event_uses_color (const char *text, int color)
|
||||
{
|
||||
const char *p;
|
||||
|
||||
if (!text || color < 0 || color > 31)
|
||||
return FALSE;
|
||||
|
||||
for (p = text; *p; p++)
|
||||
{
|
||||
int value;
|
||||
int digits;
|
||||
|
||||
if ((unsigned char) *p == 3)
|
||||
p++;
|
||||
else if (*p == '%' && p[1] == 'C')
|
||||
p += 2;
|
||||
else
|
||||
continue;
|
||||
|
||||
if (!g_ascii_isdigit (*p))
|
||||
{
|
||||
p--;
|
||||
continue;
|
||||
}
|
||||
|
||||
value = *p - '0';
|
||||
digits = 1;
|
||||
if (g_ascii_isdigit (p[1]))
|
||||
{
|
||||
value = value * 10 + p[1] - '0';
|
||||
digits = 2;
|
||||
}
|
||||
|
||||
if (value == color)
|
||||
return TRUE;
|
||||
|
||||
p += digits - 1;
|
||||
if (*p == ',' && g_ascii_isdigit (p[1]))
|
||||
{
|
||||
p++;
|
||||
value = *p - '0';
|
||||
if (g_ascii_isdigit (p[1]))
|
||||
{
|
||||
value = value * 10 + p[1] - '0';
|
||||
p++;
|
||||
}
|
||||
if (value == color)
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
char **
|
||||
text_color_event_names (int color, int *count)
|
||||
{
|
||||
GPtrArray *names;
|
||||
int found = 0;
|
||||
int i;
|
||||
|
||||
if (count)
|
||||
*count = 0;
|
||||
|
||||
names = g_ptr_array_new ();
|
||||
|
||||
for (i = 0; i < NUM_XP; i++)
|
||||
{
|
||||
const char *text = pntevts_text[i] ? pntevts_text[i] : te[i].def;
|
||||
|
||||
if (!text_event_uses_color (text, color))
|
||||
continue;
|
||||
|
||||
g_ptr_array_add (names, g_strdup (_(te[i].name)));
|
||||
found++;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
g_ptr_array_free (names, TRUE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_ptr_array_add (names, NULL);
|
||||
if (count)
|
||||
*count = found;
|
||||
return (char **) g_ptr_array_free (names, FALSE);
|
||||
}
|
||||
|
||||
static void
|
||||
pevent_check_all_loaded (void)
|
||||
{
|
||||
@@ -1809,12 +1900,97 @@ format_event (session *sess, int index, char **args, char *o, gsize sizeofo, uns
|
||||
o[0] = 0;
|
||||
}
|
||||
|
||||
static char *
|
||||
text_event_without_hostmask_format (const char *format, int host_arg)
|
||||
{
|
||||
char token[3];
|
||||
const char *arg;
|
||||
const char *open;
|
||||
const char *close;
|
||||
const char *start;
|
||||
char *out;
|
||||
gsize prefix_len;
|
||||
|
||||
g_snprintf (token, sizeof (token), "$%d", host_arg);
|
||||
arg = strstr (format, token);
|
||||
if (!arg)
|
||||
return NULL;
|
||||
|
||||
open = arg;
|
||||
while (open > format && *open != '(' && *open != '\n')
|
||||
open--;
|
||||
|
||||
close = arg + strlen (token);
|
||||
while (*close && *close != ')' && *close != '\n')
|
||||
close++;
|
||||
|
||||
if (*open != '(' || *close != ')')
|
||||
return NULL;
|
||||
|
||||
start = open;
|
||||
if (start > format && start[-1] == ' ')
|
||||
start--;
|
||||
|
||||
prefix_len = start - format;
|
||||
out = g_malloc (prefix_len + strlen (close + 1) + 1);
|
||||
memcpy (out, format, prefix_len);
|
||||
strcpy (out + prefix_len, close + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void
|
||||
display_event_string (session *sess, int event, char **args, char *format,
|
||||
unsigned int stripcolor_args, time_t timestamp)
|
||||
{
|
||||
char *compiled;
|
||||
char *saved;
|
||||
char o[4096];
|
||||
int max_arg;
|
||||
|
||||
if (pevt_build_string (format, &compiled, &max_arg) != 0)
|
||||
return;
|
||||
|
||||
saved = pntevts[event];
|
||||
pntevts[event] = compiled;
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
pntevts[event] = saved;
|
||||
g_free (compiled);
|
||||
|
||||
if (o[0])
|
||||
PrintTextTimeStamp (sess, o, timestamp);
|
||||
}
|
||||
|
||||
static void
|
||||
display_event (session *sess, int event, char **args,
|
||||
unsigned int stripcolor_args, time_t timestamp)
|
||||
{
|
||||
char o[4096];
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
char *format;
|
||||
char *host;
|
||||
int host_arg;
|
||||
|
||||
if (prefs.hex_irc_hide_join_part_hostmask &&
|
||||
(event == XP_TE_JOIN || event == XP_TE_PART || event == XP_TE_PARTREASON))
|
||||
{
|
||||
host_arg = event == XP_TE_JOIN ? 3 : 2;
|
||||
format = text_event_without_hostmask_format (pntevts_text[event], host_arg);
|
||||
if (format)
|
||||
{
|
||||
display_event_string (sess, event, args, format, stripcolor_args, timestamp);
|
||||
g_free (format);
|
||||
return;
|
||||
}
|
||||
|
||||
host = args[host_arg];
|
||||
args[host_arg] = "";
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
args[host_arg] = host;
|
||||
}
|
||||
else
|
||||
{
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
}
|
||||
|
||||
if (o[0])
|
||||
PrintTextTimeStamp (sess, o, timestamp);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ int pevt_build_string (const char *input, char **output, int *max_arg);
|
||||
int pevent_load (char *filename);
|
||||
void pevent_make_pntevts (void);
|
||||
int text_color_of (char *name);
|
||||
char **text_color_event_names (int color, int *count);
|
||||
void text_emit (int index, session *sess, char *a, char *b, char *c, char *d,
|
||||
time_t timestamp);
|
||||
int text_emit_by_name (char *name, session *sess, time_t timestamp,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
148
src/common/url.c
148
src/common/url.c
@@ -20,12 +20,14 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <glib.h>
|
||||
#include "zoitechat.h"
|
||||
#include "zoitechatc.h"
|
||||
#include "cfgfiles.h"
|
||||
#include "fe.h"
|
||||
#include "tree.h"
|
||||
#include "url.h"
|
||||
#include "public_suffix_data.h"
|
||||
#ifdef HAVE_STRINGS_H
|
||||
#include <strings.h>
|
||||
#endif
|
||||
@@ -35,6 +37,7 @@ GTree *url_btree = NULL;
|
||||
static gboolean regex_match (const GRegex *re, const char *word,
|
||||
int *start, int *end);
|
||||
static const GRegex *re_url (void);
|
||||
static const GRegex *re_url_no_scheme (void);
|
||||
static const GRegex *re_email (void);
|
||||
static const GRegex *re_nick (void);
|
||||
static const GRegex *re_channel (void);
|
||||
@@ -42,6 +45,8 @@ static gboolean match_nick (const char *word, int *start, int *end);
|
||||
static gboolean match_channel (const char *word, int *start, int *end);
|
||||
static gboolean match_url (const char *word, int *start, int *end);
|
||||
static gboolean match_email (const char *word, int *start, int *end);
|
||||
static gboolean host_has_public_suffix (const char *host);
|
||||
static gboolean host_has_public_suffix_range (const char *word, int start, int end);
|
||||
|
||||
static int
|
||||
url_free (char *url, void *data)
|
||||
@@ -266,7 +271,16 @@ match_channel (const char *word, int *start, int *end)
|
||||
static gboolean
|
||||
match_url (const char *word, int *start, int *end)
|
||||
{
|
||||
return regex_match (re_url (), word, start, end);
|
||||
if (regex_match (re_url (), word, start, end))
|
||||
return TRUE;
|
||||
|
||||
if (!regex_match (re_url_no_scheme (), word, start, end))
|
||||
return FALSE;
|
||||
|
||||
if (*start > 0 && word[*start - 1] == '@')
|
||||
return FALSE;
|
||||
|
||||
return host_has_public_suffix_range (word, *start, *end);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
@@ -307,7 +321,7 @@ url_check_line (char *buf)
|
||||
for (i = 0; i < ARRAY_SIZE (commands); i++)
|
||||
{
|
||||
char *cmd = commands[i];
|
||||
int len = strlen (cmd);
|
||||
size_t len = strlen (cmd);
|
||||
|
||||
if (strncmp (cmd, po, len) == 0)
|
||||
{
|
||||
@@ -393,6 +407,114 @@ regex_match (const GRegex *re, const char *word, int *start, int *end)
|
||||
return found;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
host_has_public_suffix_range (const char *word, int start, int end)
|
||||
{
|
||||
char *candidate;
|
||||
const char *host_start;
|
||||
const char *host_end;
|
||||
const char *host_colon;
|
||||
gboolean ok;
|
||||
int host_len;
|
||||
char *host;
|
||||
|
||||
candidate = g_strndup (word + start, end - start);
|
||||
host_start = candidate;
|
||||
host_end = candidate + strlen (candidate);
|
||||
if (*host_start == '[')
|
||||
{
|
||||
g_free (candidate);
|
||||
return FALSE;
|
||||
}
|
||||
host_colon = strchr (host_start, ':');
|
||||
if (host_colon)
|
||||
host_end = host_colon;
|
||||
host_colon = strchr (host_start, '/');
|
||||
if (host_colon && host_colon < host_end)
|
||||
host_end = host_colon;
|
||||
host_len = (int)(host_end - host_start);
|
||||
if (host_len <= 0)
|
||||
{
|
||||
g_free (candidate);
|
||||
return FALSE;
|
||||
}
|
||||
host = g_strndup (host_start, host_len);
|
||||
ok = host_has_public_suffix (host);
|
||||
g_free (host);
|
||||
g_free (candidate);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static GHashTable *
|
||||
public_suffix_table (void)
|
||||
{
|
||||
static GHashTable *table = NULL;
|
||||
unsigned int i;
|
||||
|
||||
if (table)
|
||||
return table;
|
||||
|
||||
table = g_hash_table_new (g_str_hash, g_str_equal);
|
||||
for (i = 0; i < public_suffix_rules_len; i++)
|
||||
{
|
||||
g_hash_table_add (table, (gpointer)public_suffix_rules[i]);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
host_has_public_suffix (const char *host)
|
||||
{
|
||||
GHashTable *table;
|
||||
gchar **labels;
|
||||
int i;
|
||||
int n;
|
||||
gboolean matched = FALSE;
|
||||
|
||||
if (!strchr (host, '.'))
|
||||
return FALSE;
|
||||
|
||||
labels = g_strsplit (host, ".", -1);
|
||||
for (n = 0; labels[n]; n++)
|
||||
{
|
||||
if (labels[n][0] == '\0')
|
||||
{
|
||||
g_strfreev (labels);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
table = public_suffix_table ();
|
||||
for (i = 0; i < n; i++)
|
||||
{
|
||||
char *tail = g_strjoinv (".", &labels[i]);
|
||||
if (g_hash_table_contains (table, tail))
|
||||
matched = TRUE;
|
||||
if (i + 1 < n)
|
||||
{
|
||||
char *tail_wild = g_strjoinv (".", &labels[i + 1]);
|
||||
char *wild = g_strconcat ("*.", tail_wild, NULL);
|
||||
if (g_hash_table_contains (table, wild))
|
||||
matched = TRUE;
|
||||
g_free (tail_wild);
|
||||
g_free (wild);
|
||||
}
|
||||
if (i > 0)
|
||||
{
|
||||
char *exc = g_strconcat ("!", tail, NULL);
|
||||
if (g_hash_table_contains (table, exc))
|
||||
matched = TRUE;
|
||||
g_free (exc);
|
||||
}
|
||||
g_free (tail);
|
||||
if (matched)
|
||||
break;
|
||||
}
|
||||
|
||||
g_strfreev (labels);
|
||||
return matched;
|
||||
}
|
||||
|
||||
/* Miscellaneous description --- */
|
||||
#define DOMAIN_LABEL "[\\pL\\pN](?:[-\\pL\\pN]{0,61}[\\pL\\pN])?"
|
||||
#define DOMAIN DOMAIN_LABEL "(\\." DOMAIN_LABEL ")*"
|
||||
@@ -477,6 +599,28 @@ re_url (void)
|
||||
return url_ret;
|
||||
}
|
||||
|
||||
static const GRegex *
|
||||
re_url_no_scheme (void)
|
||||
{
|
||||
static GRegex *url_ret = NULL;
|
||||
GString *grist_gstr;
|
||||
char *grist;
|
||||
|
||||
if (url_ret) return url_ret;
|
||||
|
||||
grist_gstr = g_string_new (NULL);
|
||||
g_string_append (grist_gstr, "(");
|
||||
g_string_append (grist_gstr, HOST_URL_OPT_TLD OPT_PORT);
|
||||
g_string_append_printf (grist_gstr, "(/" PATH ")?");
|
||||
g_string_append (grist_gstr, ")");
|
||||
|
||||
grist = g_string_free (grist_gstr, FALSE);
|
||||
url_ret = make_re (grist);
|
||||
g_free (grist);
|
||||
|
||||
return url_ret;
|
||||
}
|
||||
|
||||
#define EMAIL_LOCAL_ATOM "[\\pL\\pN!#$%&'*+/=?^_`{|}~-]+"
|
||||
#define EMAIL_LOCAL EMAIL_LOCAL_ATOM "(\\." EMAIL_LOCAL_ATOM ")*"
|
||||
#define EMAIL EMAIL_LOCAL "@" DOMAIN TLD
|
||||
|
||||
@@ -31,6 +31,7 @@ struct User
|
||||
char *servername;
|
||||
char *account;
|
||||
time_t lasttalk;
|
||||
time_t typing_time;
|
||||
unsigned int access; /* axs bit field */
|
||||
char prefix[2]; /* @ + % */
|
||||
unsigned int op:1;
|
||||
@@ -39,6 +40,7 @@ struct User
|
||||
unsigned int me:1;
|
||||
unsigned int away:1;
|
||||
unsigned int selected:1;
|
||||
unsigned int typing:2;
|
||||
};
|
||||
|
||||
#define USERACCESS_SIZE 12
|
||||
|
||||
@@ -98,7 +98,7 @@ path_part (char *file, char *path, int pathlen)
|
||||
char * /* like strstr(), but nocase */
|
||||
nocasestrstr (const char *s, const char *wanted)
|
||||
{
|
||||
register const int len = strlen (wanted);
|
||||
register const size_t len = strlen (wanted);
|
||||
|
||||
if (len == 0)
|
||||
return (char *)s;
|
||||
@@ -472,6 +472,34 @@ get_cpu_arch (void)
|
||||
return sysinfo_get_build_arch ();
|
||||
}
|
||||
|
||||
/* RtlGetVersion is used because GetVersionEx reports a version capped by
|
||||
* the compatibility manifest rather than the real one. */
|
||||
gboolean
|
||||
win32_is_windows_8_or_newer (void)
|
||||
{
|
||||
typedef LONG (WINAPI *rtl_get_version_func) (PRTL_OSVERSIONINFOW);
|
||||
rtl_get_version_func rtl_get_version;
|
||||
RTL_OSVERSIONINFOW version;
|
||||
HMODULE ntdll;
|
||||
|
||||
ntdll = GetModuleHandleW (L"ntdll.dll");
|
||||
if (ntdll == NULL)
|
||||
return FALSE;
|
||||
|
||||
rtl_get_version = (rtl_get_version_func) GetProcAddress (ntdll, "RtlGetVersion");
|
||||
if (rtl_get_version == NULL)
|
||||
return FALSE;
|
||||
|
||||
memset (&version, 0, sizeof (version));
|
||||
version.dwOSVersionInfoSize = sizeof (version);
|
||||
if (rtl_get_version (&version) != 0)
|
||||
return FALSE;
|
||||
|
||||
/* Windows 8 is 6.2; Windows 7 is 6.1 */
|
||||
return version.dwMajorVersion > 6 ||
|
||||
(version.dwMajorVersion == 6 && version.dwMinorVersion >= 2);
|
||||
}
|
||||
|
||||
char *
|
||||
get_sys_str (int with_cpu)
|
||||
{
|
||||
|
||||
@@ -65,6 +65,7 @@ int waitline (int sok, char *buf, int bufsize, int);
|
||||
#ifdef WIN32
|
||||
int waitline2 (GIOChannel *source, char *buf, int bufsize);
|
||||
int get_cpu_arch (void);
|
||||
gboolean win32_is_windows_8_or_newer (void);
|
||||
#else
|
||||
#define waitline2(source,buf,size) waitline(serv->childread,buf,size,0)
|
||||
#endif
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* Copyright (C) 1998-2010 Peter Zelezny.
|
||||
* Copyright (C) 2009-2013 Berke Viktor.
|
||||
*
|
||||
* 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 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
|
||||
@@ -13,8 +13,8 @@
|
||||
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
* along with this program; if not, see
|
||||
* <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* You can distribute this header with your plugins for easy compilation */
|
||||
|
||||
@@ -379,6 +379,8 @@ lag_check (void)
|
||||
char tbuf[128];
|
||||
time_t now = time (0);
|
||||
time_t lag;
|
||||
time_t ping_age;
|
||||
unsigned long ping_timeout;
|
||||
|
||||
tim = make_ping_time ();
|
||||
|
||||
@@ -388,8 +390,11 @@ lag_check (void)
|
||||
if (serv->connected && serv->end_of_motd)
|
||||
{
|
||||
lag = now - serv->ping_recv;
|
||||
if (prefs.hex_net_ping_timeout != 0 && lag > prefs.hex_net_ping_timeout && lag > 0)
|
||||
ping_timeout = (unsigned long) prefs.hex_net_ping_timeout * 1000;
|
||||
if (serv->lag_sent && prefs.hex_net_ping_timeout != 0
|
||||
&& tim - serv->lag_sent > ping_timeout)
|
||||
{
|
||||
lag = (tim - serv->lag_sent) / 1000;
|
||||
sprintf (tbuf, "%" G_GINT64_FORMAT, (gint64) lag);
|
||||
EMIT_SIGNAL (XP_TE_PINGTIMEOUT, serv->server_session, tbuf, NULL,
|
||||
NULL, NULL, 0);
|
||||
@@ -398,11 +403,11 @@ lag_check (void)
|
||||
}
|
||||
else
|
||||
{
|
||||
g_snprintf (tbuf, sizeof (tbuf), "LAG%lu", tim);
|
||||
serv->p_ping (serv, "", tbuf);
|
||||
|
||||
if (!serv->lag_sent)
|
||||
ping_age = now - serv->ping_recv;
|
||||
if (!serv->lag_sent && prefs.hex_net_lag_check > 0 && ping_age >= prefs.hex_net_lag_check)
|
||||
{
|
||||
g_snprintf (tbuf, sizeof (tbuf), "LAG%lu", tim);
|
||||
serv->p_ping (serv, "", tbuf);
|
||||
serv->lag_sent = tim;
|
||||
fe_set_lag (serv, -1);
|
||||
}
|
||||
@@ -525,7 +530,7 @@ zoitechat_reinit_timers (void)
|
||||
if ((prefs.hex_net_ping_timeout != 0 || prefs.hex_gui_lagometer)
|
||||
&& lag_check_tag == 0)
|
||||
{
|
||||
lag_check_tag = fe_timeout_add_seconds (30, zoitechat_lag_check, NULL);
|
||||
lag_check_tag = fe_timeout_add_seconds (1, zoitechat_lag_check, NULL);
|
||||
}
|
||||
else if ((!prefs.hex_net_ping_timeout && !prefs.hex_gui_lagometer)
|
||||
&& lag_check_tag != 0)
|
||||
@@ -792,7 +797,13 @@ session_free (session *killsess)
|
||||
|
||||
send_quit_or_part (killsess);
|
||||
|
||||
if (killsess->typing_timeout_tag)
|
||||
fe_timeout_remove (killsess->typing_timeout_tag);
|
||||
if (killsess->typing_animation_tag)
|
||||
fe_timeout_remove (killsess->typing_animation_tag);
|
||||
|
||||
history_free (&killsess->history);
|
||||
reply_cache_free (killsess);
|
||||
g_free (killsess->topic);
|
||||
g_free (killsess->current_modes);
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_gui_input_style;
|
||||
unsigned int hex_gui_join_dialog;
|
||||
unsigned int hex_gui_mode_buttons;
|
||||
unsigned int hex_gui_mode_buttons_inline;
|
||||
unsigned int hex_gui_quit_dialog;
|
||||
/* unsigned int hex_gui_single; */
|
||||
unsigned int hex_gui_slist_fav;
|
||||
@@ -148,6 +149,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_gui_tab_sort;
|
||||
unsigned int hex_gui_tab_utils;
|
||||
unsigned int hex_gui_topicbar;
|
||||
unsigned int hex_gui_topicbar_multiline;
|
||||
unsigned int hex_gui_tray;
|
||||
unsigned int hex_gui_tray_away;
|
||||
unsigned int hex_gui_tray_blink;
|
||||
@@ -188,6 +190,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_irc_conf_mode;
|
||||
unsigned int hex_irc_hidehost;
|
||||
unsigned int hex_irc_hide_nickchange;
|
||||
unsigned int hex_irc_hide_join_part_hostmask;
|
||||
unsigned int hex_irc_hide_version;
|
||||
unsigned int hex_irc_invisible;
|
||||
unsigned int hex_irc_logging;
|
||||
@@ -256,6 +259,7 @@ struct zoitechatprefs
|
||||
int hex_gui_dialog_left;
|
||||
int hex_gui_dialog_top;
|
||||
int hex_gui_dialog_width;
|
||||
int hex_gui_emoji_skin_tone;
|
||||
int hex_gui_lagometer;
|
||||
int hex_gui_lang;
|
||||
int hex_gui_pane_divider_position;
|
||||
@@ -267,6 +271,7 @@ struct zoitechatprefs
|
||||
int hex_gui_tab_layout;
|
||||
int hex_gui_tab_closebuttons;
|
||||
int hex_gui_tab_middleclose;
|
||||
int hex_gui_mouse_scroll_speed;
|
||||
int hex_gui_tab_newtofront;
|
||||
int hex_gui_tab_pos;
|
||||
int hex_gui_tab_small;
|
||||
@@ -289,6 +294,10 @@ struct zoitechatprefs
|
||||
int hex_irc_join_delay;
|
||||
int hex_irc_notice_pos;
|
||||
int hex_net_ping_timeout;
|
||||
int hex_net_lag_check;
|
||||
int hex_net_keepalive_idle;
|
||||
int hex_net_keepalive_interval;
|
||||
int hex_net_keepalive_count;
|
||||
int hex_net_proxy_port;
|
||||
int hex_net_proxy_type; /* 0=disabled, 1=wingate 2=socks4, 3=socks5, 4=http */
|
||||
int hex_net_proxy_use; /* 0=all 1=IRC_ONLY 2=DCC_ONLY */
|
||||
@@ -380,6 +389,14 @@ typedef enum {
|
||||
TAB_STATE_NEW_HILIGHT = (1 << 2),
|
||||
} tab_state_flags;
|
||||
|
||||
typedef struct reply_item
|
||||
{
|
||||
char *msgid;
|
||||
char *nick;
|
||||
char *text;
|
||||
time_t timestamp;
|
||||
} reply_item;
|
||||
|
||||
typedef struct session
|
||||
{
|
||||
/* Per-Channel Alerts */
|
||||
@@ -421,8 +438,17 @@ typedef struct session
|
||||
char *quitreason;
|
||||
char *topic;
|
||||
char *current_modes; /* free() me */
|
||||
GSList *reply_items;
|
||||
char *reply_msgid;
|
||||
char *reply_target;
|
||||
char *reply_nick;
|
||||
char *reply_text;
|
||||
|
||||
int mode_timeout_tag;
|
||||
int typing_timeout_tag;
|
||||
int typing_status;
|
||||
int typing_animation_tag;
|
||||
int typing_animation_frame;
|
||||
|
||||
struct session *lastlog_sess;
|
||||
struct nbexec *running_exec;
|
||||
@@ -486,6 +512,8 @@ typedef struct server
|
||||
void (*p_set_back)(struct server *);
|
||||
void (*p_set_away)(struct server *, char *reason);
|
||||
void (*p_message)(struct server *, char *channel, char *text);
|
||||
void (*p_message_tagged)(struct server *, char *tags, char *channel, char *text);
|
||||
void (*p_tagmsg)(struct server *, char *tags, char *target);
|
||||
void (*p_action)(struct server *, char *channel, char *act);
|
||||
void (*p_notice)(struct server *, char *channel, char *text);
|
||||
void (*p_topic)(struct server *, char *channel, char *topic);
|
||||
@@ -535,6 +563,7 @@ typedef struct server
|
||||
int loginmethod; /* see login_types[] */
|
||||
|
||||
char *chantypes; /* for 005 numeric - free me */
|
||||
char *clienttagdeny;
|
||||
char *chanmodes; /* for 005 numeric - free me */
|
||||
char *nick_prefixes; /* e.g. "*@%+" */
|
||||
char *nick_modes; /* e.g. "aohv" */
|
||||
@@ -597,6 +626,8 @@ typedef struct server
|
||||
unsigned int have_extjoin:1; /* cap extended-join */
|
||||
unsigned int have_account_tag:1; /* cap account-tag */
|
||||
unsigned int have_server_time:1; /* cap server-time */
|
||||
unsigned int have_message_tags:1;
|
||||
unsigned int have_echo_message:1;
|
||||
unsigned int have_sasl:1; /* SASL capability */
|
||||
unsigned int have_except:1; /* ban exemptions +e */
|
||||
unsigned int have_invite:1; /* invite exemptions +I */
|
||||
|
||||
@@ -452,7 +452,7 @@ void
|
||||
fe_add_chan_list (server *serv, char *chan, char *users, char *topic)
|
||||
{
|
||||
chanlistrow *next_row;
|
||||
int len = strlen (chan) + 1;
|
||||
size_t len = strlen (chan) + 1;
|
||||
|
||||
/* we allocate the struct and channel string in one go */
|
||||
next_row = g_malloc (sizeof (chanlistrow) + len);
|
||||
|
||||
@@ -61,28 +61,19 @@ static tab_scroll_animation *tab_right_animation;
|
||||
*/
|
||||
|
||||
static inline gint
|
||||
cv_tabs_get_viewport_size (GdkWindow *parent_win, gboolean vertical)
|
||||
cv_tabs_get_viewport_size (GtkAdjustment *adj)
|
||||
{
|
||||
gint viewport_size = 0;
|
||||
|
||||
if (vertical)
|
||||
viewport_size = gdk_window_get_height (parent_win);
|
||||
else
|
||||
viewport_size = gdk_window_get_width (parent_win);
|
||||
|
||||
return viewport_size;
|
||||
return (gint) gtk_adjustment_get_page_size (adj);
|
||||
}
|
||||
|
||||
static void
|
||||
cv_tabs_sizealloc (GtkWidget *widget, GtkAllocation *allocation, chanview *cv)
|
||||
{
|
||||
GdkWindow *parent_win;
|
||||
GtkAdjustment *adj;
|
||||
GtkWidget *inner;
|
||||
gint viewport_size;
|
||||
|
||||
inner = ((tabview *)cv)->inner;
|
||||
parent_win = gtk_widget_get_window (gtk_widget_get_parent (inner));
|
||||
|
||||
if (cv->vertical)
|
||||
{
|
||||
@@ -92,16 +83,20 @@ cv_tabs_sizealloc (GtkWidget *widget, GtkAllocation *allocation, chanview *cv)
|
||||
adj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (gtk_widget_get_parent (inner)));
|
||||
}
|
||||
|
||||
viewport_size = cv_tabs_get_viewport_size (parent_win, cv->vertical);
|
||||
viewport_size = cv_tabs_get_viewport_size (adj);
|
||||
|
||||
if (gtk_adjustment_get_upper (adj) <= viewport_size)
|
||||
{
|
||||
gtk_widget_hide (((tabview *)cv)->b1);
|
||||
gtk_widget_hide (((tabview *)cv)->b2);
|
||||
if (((tabview *)cv)->b1)
|
||||
gtk_widget_hide (((tabview *)cv)->b1);
|
||||
if (((tabview *)cv)->b2)
|
||||
gtk_widget_hide (((tabview *)cv)->b2);
|
||||
} else
|
||||
{
|
||||
gtk_widget_show (((tabview *)cv)->b1);
|
||||
gtk_widget_show (((tabview *)cv)->b2);
|
||||
if (((tabview *)cv)->b1)
|
||||
gtk_widget_show (((tabview *)cv)->b1);
|
||||
if (((tabview *)cv)->b2)
|
||||
gtk_widget_show (((tabview *)cv)->b2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,10 +236,8 @@ tab_scroll_left_up_clicked (GtkWidget *widget, chanview *cv)
|
||||
gint viewport_size;
|
||||
gfloat new_value;
|
||||
GtkWidget *inner;
|
||||
GdkWindow *parent_win;
|
||||
|
||||
inner = ((tabview *)cv)->inner;
|
||||
parent_win = gtk_widget_get_window (gtk_widget_get_parent (inner));
|
||||
|
||||
if (cv->vertical)
|
||||
{
|
||||
@@ -254,7 +247,7 @@ tab_scroll_left_up_clicked (GtkWidget *widget, chanview *cv)
|
||||
adj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (gtk_widget_get_parent(inner)));
|
||||
}
|
||||
|
||||
viewport_size = cv_tabs_get_viewport_size (parent_win, cv->vertical);
|
||||
viewport_size = cv_tabs_get_viewport_size (adj);
|
||||
|
||||
new_value = tab_search_offset (inner, gtk_adjustment_get_value (adj), 0, cv->vertical);
|
||||
|
||||
@@ -279,10 +272,8 @@ tab_scroll_right_down_clicked (GtkWidget *widget, chanview *cv)
|
||||
gint viewport_size;
|
||||
gfloat new_value;
|
||||
GtkWidget *inner;
|
||||
GdkWindow *parent_win;
|
||||
|
||||
inner = ((tabview *)cv)->inner;
|
||||
parent_win = gtk_widget_get_window (gtk_widget_get_parent (inner));
|
||||
|
||||
if (cv->vertical)
|
||||
{
|
||||
@@ -292,7 +283,7 @@ tab_scroll_right_down_clicked (GtkWidget *widget, chanview *cv)
|
||||
adj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (gtk_widget_get_parent(inner)));
|
||||
}
|
||||
|
||||
viewport_size = cv_tabs_get_viewport_size (parent_win, cv->vertical);
|
||||
viewport_size = cv_tabs_get_viewport_size (adj);
|
||||
|
||||
new_value = tab_search_offset (inner, gtk_adjustment_get_value (adj), 1, cv->vertical);
|
||||
|
||||
@@ -313,69 +304,44 @@ tab_scroll_right_down_clicked (GtkWidget *widget, chanview *cv)
|
||||
static gboolean
|
||||
tab_scroll_cb (GtkWidget *widget, GdkEventScroll *event, gpointer cv)
|
||||
{
|
||||
int direction = cv_scroll_direction (event);
|
||||
int i;
|
||||
|
||||
if (prefs.hex_gui_tab_scrollchans)
|
||||
{
|
||||
if (event->direction == GDK_SCROLL_DOWN)
|
||||
mg_switch_page (1, 1);
|
||||
else if (event->direction == GDK_SCROLL_UP)
|
||||
mg_switch_page (1, -1);
|
||||
if (direction != 0)
|
||||
{
|
||||
for (i = 0; i < cv_scroll_step_count (); i++)
|
||||
mg_switch_page (1, direction);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* mouse wheel scrolling */
|
||||
if (event->direction == GDK_SCROLL_UP)
|
||||
tab_scroll_left_up_clicked (widget, cv);
|
||||
else if (event->direction == GDK_SCROLL_DOWN)
|
||||
tab_scroll_right_down_clicked (widget, cv);
|
||||
if (direction < 0)
|
||||
{
|
||||
for (i = 0; i < cv_scroll_step_count (); i++)
|
||||
tab_scroll_left_up_clicked (widget, cv);
|
||||
return TRUE;
|
||||
}
|
||||
else if (direction > 0)
|
||||
{
|
||||
for (i = 0; i < cv_scroll_step_count (); i++)
|
||||
tab_scroll_right_down_clicked (widget, cv);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* make a Scroll (arrow) button */
|
||||
|
||||
static GtkWidget *
|
||||
make_sbutton (GtkArrowType type, void *click_cb, void *userdata)
|
||||
{
|
||||
GtkWidget *button, *arrow;
|
||||
const char *icon_name = "pan-end-symbolic";
|
||||
|
||||
button = gtk_button_new ();
|
||||
switch (type)
|
||||
{
|
||||
case GTK_ARROW_UP:
|
||||
icon_name = "pan-up-symbolic";
|
||||
break;
|
||||
case GTK_ARROW_DOWN:
|
||||
icon_name = "pan-down-symbolic";
|
||||
break;
|
||||
case GTK_ARROW_LEFT:
|
||||
icon_name = "pan-start-symbolic";
|
||||
break;
|
||||
case GTK_ARROW_RIGHT:
|
||||
default:
|
||||
icon_name = "pan-end-symbolic";
|
||||
break;
|
||||
}
|
||||
|
||||
arrow = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_BUTTON);
|
||||
gtk_container_add (GTK_CONTAINER (button), arrow);
|
||||
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
|
||||
g_signal_connect (G_OBJECT (button), "clicked",
|
||||
G_CALLBACK (click_cb), userdata);
|
||||
g_signal_connect (G_OBJECT (button), "scroll-event",
|
||||
G_CALLBACK (tab_scroll_cb), userdata);
|
||||
gtk_widget_show (arrow);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
static void
|
||||
cv_tabs_init (chanview *cv)
|
||||
{
|
||||
GtkWidget *box, *hbox = NULL;
|
||||
GtkWidget *box;
|
||||
GtkWidget *viewport;
|
||||
GtkWidget *outer;
|
||||
GtkWidget *tree;
|
||||
|
||||
if (cv->vertical)
|
||||
{
|
||||
@@ -390,12 +356,19 @@ cv_tabs_init (chanview *cv)
|
||||
G_CALLBACK (cv_tabs_sizealloc), cv);
|
||||
gtk_widget_show (outer);
|
||||
|
||||
viewport = gtk_viewport_new (0, 0);
|
||||
gtk_viewport_set_shadow_type (GTK_VIEWPORT (viewport), GTK_SHADOW_NONE);
|
||||
viewport = gtk_scrolled_window_new (0, 0);
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (viewport), GTK_SHADOW_NONE);
|
||||
if (cv->vertical)
|
||||
gtk_widget_set_size_request (viewport, -1, 1);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (viewport),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
else
|
||||
gtk_widget_set_size_request (viewport, 1, -1);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (viewport),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER);
|
||||
gtk_scrolled_window_set_min_content_width (GTK_SCROLLED_WINDOW (viewport), 1);
|
||||
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (viewport), 1);
|
||||
gtk_widget_set_hexpand (viewport, TRUE);
|
||||
gtk_widget_set_vexpand (viewport, TRUE);
|
||||
cv_add_scroll_events (viewport);
|
||||
g_signal_connect (G_OBJECT (viewport), "scroll-event",
|
||||
G_CALLBACK (tab_scroll_cb), cv);
|
||||
gtk_box_pack_start (GTK_BOX (outer), viewport, 1, 1, 0);
|
||||
@@ -413,34 +386,10 @@ cv_tabs_init (chanview *cv)
|
||||
gtk_container_add (GTK_CONTAINER (viewport), box);
|
||||
gtk_widget_show (box);
|
||||
|
||||
/* if vertical, the buttons can be side by side */
|
||||
if (cv->vertical)
|
||||
{
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (outer), hbox, 0, 0, 0);
|
||||
gtk_widget_show (hbox);
|
||||
}
|
||||
|
||||
/* make the Scroll buttons */
|
||||
((tabview *)cv)->b2 = make_sbutton (cv->vertical ?
|
||||
GTK_ARROW_UP : GTK_ARROW_LEFT,
|
||||
tab_scroll_left_up_clicked,
|
||||
cv);
|
||||
|
||||
((tabview *)cv)->b1 = make_sbutton (cv->vertical ?
|
||||
GTK_ARROW_DOWN : GTK_ARROW_RIGHT,
|
||||
tab_scroll_right_down_clicked,
|
||||
cv);
|
||||
|
||||
if (hbox)
|
||||
{
|
||||
gtk_container_add (GTK_CONTAINER (hbox), ((tabview *)cv)->b2);
|
||||
gtk_container_add (GTK_CONTAINER (hbox), ((tabview *)cv)->b1);
|
||||
} else
|
||||
{
|
||||
gtk_box_pack_start (GTK_BOX (outer), ((tabview *)cv)->b2, 0, 0, 0);
|
||||
gtk_box_pack_start (GTK_BOX (outer), ((tabview *)cv)->b1, 0, 0, 0);
|
||||
}
|
||||
tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (cv->store));
|
||||
gtk_widget_set_name (tree, "zoitechat-tree");
|
||||
gtk_widget_set_no_show_all (tree, TRUE);
|
||||
gtk_box_pack_start (GTK_BOX (outer), tree, 0, 0, 0);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (cv->box), outer);
|
||||
}
|
||||
@@ -746,9 +695,11 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
||||
gtk_widget_set_name (but, "zoitechat-tab");
|
||||
gtk_widget_set_size_request (but, -1, 14);
|
||||
gtk_widget_add_events (but, GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
|
||||
cv_add_scroll_events (but);
|
||||
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2);
|
||||
label = gtk_label_new (name);
|
||||
close_button = gtk_button_new ();
|
||||
cv_add_scroll_events (close_button);
|
||||
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "flat");
|
||||
close_icon = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
|
||||
gtk_image_set_pixel_size (GTK_IMAGE (close_icon), 8);
|
||||
@@ -764,6 +715,10 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
||||
/* used to trap right-clicks */
|
||||
g_signal_connect (G_OBJECT (but), "button-press-event",
|
||||
G_CALLBACK (tab_click_cb), ch);
|
||||
g_signal_connect (G_OBJECT (but), "scroll-event",
|
||||
G_CALLBACK (tab_scroll_cb), cv);
|
||||
g_signal_connect (G_OBJECT (close_button), "scroll-event",
|
||||
G_CALLBACK (tab_scroll_cb), cv);
|
||||
g_signal_connect (G_OBJECT (but), "motion-notify-event",
|
||||
G_CALLBACK (tab_close_motion_cb), ch);
|
||||
g_signal_connect (G_OBJECT (but), "leave-notify-event",
|
||||
|
||||
@@ -111,12 +111,14 @@ cv_tree_scroll_event_cb (GtkWidget *widget, GdkEventScroll *event, gpointer user
|
||||
{
|
||||
if (prefs.hex_gui_tab_scrollchans)
|
||||
{
|
||||
if (event->direction == GDK_SCROLL_DOWN)
|
||||
mg_switch_page (1, 1);
|
||||
else if (event->direction == GDK_SCROLL_UP)
|
||||
mg_switch_page (1, -1);
|
||||
int direction = cv_scroll_direction (event);
|
||||
int i;
|
||||
|
||||
return TRUE;
|
||||
if (direction != 0)
|
||||
for (i = 0; i < cv_scroll_step_count (); i++)
|
||||
mg_switch_page (1, direction);
|
||||
|
||||
return direction != 0;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
@@ -141,6 +143,7 @@ cv_tree_init (chanview *cv)
|
||||
win = gtk_scrolled_window_new (0, 0);
|
||||
gtk_widget_set_hexpand (win, TRUE);
|
||||
gtk_widget_set_vexpand (win, TRUE);
|
||||
cv_add_scroll_events (win);
|
||||
|
||||
/*gtk_container_set_border_width (GTK_CONTAINER (win), 1);*/
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (win),
|
||||
@@ -154,6 +157,7 @@ cv_tree_init (chanview *cv)
|
||||
view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (cv->store));
|
||||
gtk_widget_set_hexpand (view, TRUE);
|
||||
gtk_widget_set_vexpand (view, TRUE);
|
||||
cv_add_scroll_events (view);
|
||||
gtk_widget_set_name (view, "zoitechat-tree");
|
||||
{
|
||||
ThemeWidgetStyleValues style_values;
|
||||
|
||||
@@ -79,6 +79,8 @@ struct _chanview
|
||||
guint theme_listener_id;
|
||||
};
|
||||
|
||||
static void chanview_set_font_desc (chanview *cv, const PangoFontDescription *font_desc);
|
||||
|
||||
struct _chan
|
||||
{
|
||||
chanview *cv; /* our owner */
|
||||
@@ -95,6 +97,46 @@ static chan *cv_find_chan_by_number (chanview *cv, int num);
|
||||
static int cv_find_number_of_chan (chanview *cv, chan *find_ch);
|
||||
static void cv_find_neighbors_for_removal (chanview *cv, chan *find_ch, chan **left_ch, chan **first_ch);
|
||||
|
||||
static void
|
||||
cv_add_scroll_events (GtkWidget *widget)
|
||||
{
|
||||
gtk_widget_add_events (widget, GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
|
||||
}
|
||||
|
||||
static int
|
||||
cv_scroll_direction (GdkEventScroll *event)
|
||||
{
|
||||
gdouble dx;
|
||||
gdouble dy;
|
||||
|
||||
if (event->direction == GDK_SCROLL_SMOOTH && gdk_event_get_scroll_deltas ((GdkEvent *) event, &dx, &dy))
|
||||
{
|
||||
if (dy > 0)
|
||||
return 1;
|
||||
if (dy < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (event->direction)
|
||||
{
|
||||
case GDK_SCROLL_DOWN:
|
||||
return 1;
|
||||
case GDK_SCROLL_UP:
|
||||
return -1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
cv_scroll_step_count (void)
|
||||
{
|
||||
int speed = prefs.hex_gui_mouse_scroll_speed;
|
||||
if (speed < 1)
|
||||
speed = 1;
|
||||
return (speed + 9) / 10;
|
||||
}
|
||||
|
||||
|
||||
/* ======= TABS ======= */
|
||||
|
||||
@@ -129,9 +171,10 @@ chanview_apply_theme (chanview *cv)
|
||||
w = GTK_WIDGET (tv->tree);
|
||||
if (input_style)
|
||||
font = input_style->font_desc;
|
||||
chanview_set_font_desc (cv, font);
|
||||
|
||||
theme_manager_apply_channel_tree_style (w,
|
||||
theme_manager_get_channel_tree_palette_behavior (font));
|
||||
theme_manager_get_channel_tree_palette_behavior (cv->font_desc));
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -273,6 +316,14 @@ chanview_free_ch (chanview *cv, GtkTreeIter *iter)
|
||||
g_free (ch);
|
||||
}
|
||||
|
||||
static void
|
||||
chanview_set_font_desc (chanview *cv, const PangoFontDescription *font_desc)
|
||||
{
|
||||
if (cv->font_desc)
|
||||
pango_font_description_free (cv->font_desc);
|
||||
cv->font_desc = font_desc ? pango_font_description_copy (font_desc) : NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
chanview_destroy_store (chanview *cv) /* free every (chan *) in the store */
|
||||
{
|
||||
@@ -295,6 +346,9 @@ chanview_destroy (chanview *cv)
|
||||
if (cv->box)
|
||||
gtk_widget_destroy (cv->box);
|
||||
|
||||
if (cv->font_desc)
|
||||
pango_font_description_free (cv->font_desc);
|
||||
|
||||
chanview_destroy_store (cv);
|
||||
g_free (cv);
|
||||
}
|
||||
@@ -316,7 +370,7 @@ chanview_new (int type, int trunc_len, gboolean sort, gboolean use_icons,
|
||||
cv = g_new0 (chanview, 1);
|
||||
cv->store = gtk_tree_store_new (5, G_TYPE_STRING, G_TYPE_POINTER,
|
||||
PANGO_TYPE_ATTR_LIST, GDK_TYPE_PIXBUF, G_TYPE_INT);
|
||||
cv->font_desc = font_desc;
|
||||
cv->font_desc = font_desc ? pango_font_description_copy (font_desc) : NULL;
|
||||
cv->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
cv->trunc_len = trunc_len;
|
||||
cv->sorted = sort;
|
||||
|
||||
779
src/fe-gtk/emoji-picker.c
Normal file
779
src/fe-gtk/emoji-picker.c
Normal file
@@ -0,0 +1,779 @@
|
||||
/* ZoiteChat
|
||||
* Copyright (C) 2026 deepend-tildeclub.
|
||||
*
|
||||
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
/* ZoiteChat's own emoji picker. Replaces GTK's built-in "show-emoji-icon"
|
||||
* entry machinery so that every build offers the same catalog (bundled at
|
||||
* compile time from pinned Unicode/CLDR data, see src/common/emoji-data.h)
|
||||
* regardless of the GTK runtime version or distribution emoji data.
|
||||
* Selecting an emoji inserts the plain Unicode sequence at the cursor -
|
||||
* nothing about what is sent over IRC changes. */
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "fe-gtk.h"
|
||||
|
||||
#include "../common/zoitechat.h"
|
||||
#include "../common/zoitechatc.h"
|
||||
#include "../common/cfgfiles.h"
|
||||
#include "../common/emoji-data.h"
|
||||
|
||||
#include "emoji-picker.h"
|
||||
#include "preferences-persistence.h"
|
||||
|
||||
#define EMOJI_CATEGORY_RECENT -1
|
||||
#define EMOJI_RECENTS_MAX 32
|
||||
#define EMOJI_SEARCH_MAX_RESULTS 256
|
||||
#define EMOJI_GRID_COLUMNS 9
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWidget *button;
|
||||
GtkWidget *target; /* weak pointer to the input entry */
|
||||
GtkWidget *popover;
|
||||
GtkWidget *search_entry;
|
||||
GtkWidget *stack;
|
||||
GtkWidget *grid; /* GtkFlowBox */
|
||||
GtkWidget *scroller;
|
||||
GtkWidget *empty_label;
|
||||
GtkWidget *category_buttons[EMOJI_GROUP_COUNT + 1]; /* [0] = recents */
|
||||
GtkWidget *tone_buttons[EMOJI_TONE_COUNT];
|
||||
int category; /* EMOJI_CATEGORY_RECENT or EmojiGroup */
|
||||
gboolean populated;
|
||||
gboolean updating_buttons;
|
||||
} EmojiPicker;
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Recently used emoji, shared by all windows and persisted across runs
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
static GPtrArray *recent_emoji = NULL;
|
||||
|
||||
static char *
|
||||
emoji_recents_filename (void)
|
||||
{
|
||||
return g_build_filename (get_xdir (), "emoji-recents.conf", NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_recents_load (void)
|
||||
{
|
||||
char *filename, *contents = NULL;
|
||||
char **lines;
|
||||
gsize i;
|
||||
|
||||
if (recent_emoji)
|
||||
return;
|
||||
|
||||
recent_emoji = g_ptr_array_new_with_free_func (g_free);
|
||||
|
||||
filename = emoji_recents_filename ();
|
||||
if (g_file_get_contents (filename, &contents, NULL, NULL))
|
||||
{
|
||||
lines = g_strsplit (contents, "\n", -1);
|
||||
for (i = 0; lines[i] && recent_emoji->len < EMOJI_RECENTS_MAX; i++)
|
||||
{
|
||||
g_strstrip (lines[i]);
|
||||
/* only keep sequences that exist in the bundled catalog */
|
||||
if (*lines[i] && emoji_data_find_by_sequence (lines[i], NULL))
|
||||
g_ptr_array_add (recent_emoji, g_strdup (lines[i]));
|
||||
}
|
||||
g_strfreev (lines);
|
||||
g_free (contents);
|
||||
}
|
||||
g_free (filename);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_recents_save (void)
|
||||
{
|
||||
GString *contents = g_string_new (NULL);
|
||||
char *filename;
|
||||
gsize i;
|
||||
|
||||
for (i = 0; i < recent_emoji->len; i++)
|
||||
{
|
||||
g_string_append (contents, g_ptr_array_index (recent_emoji, i));
|
||||
g_string_append_c (contents, '\n');
|
||||
}
|
||||
|
||||
filename = emoji_recents_filename ();
|
||||
g_file_set_contents (filename, contents->str, contents->len, NULL);
|
||||
g_free (filename);
|
||||
g_string_free (contents, TRUE);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_recents_add (const char *sequence)
|
||||
{
|
||||
GPtrArray *updated;
|
||||
gsize i;
|
||||
|
||||
emoji_recents_load ();
|
||||
|
||||
updated = g_ptr_array_new_with_free_func (g_free);
|
||||
g_ptr_array_add (updated, g_strdup (sequence));
|
||||
for (i = 0; i < recent_emoji->len && updated->len < EMOJI_RECENTS_MAX; i++)
|
||||
{
|
||||
const char *old = g_ptr_array_index (recent_emoji, i);
|
||||
|
||||
if (strcmp (old, sequence) != 0)
|
||||
g_ptr_array_add (updated, g_strdup (old));
|
||||
}
|
||||
g_ptr_array_free (recent_emoji, TRUE);
|
||||
recent_emoji = updated;
|
||||
|
||||
emoji_recents_save ();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Appearance helpers
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/* One application-wide provider so every emoji cell prefers a color emoji
|
||||
* font without changing the fonts of surrounding widgets. Emoji families
|
||||
* that are not installed are simply skipped by fontconfig/Pango. */
|
||||
static void
|
||||
emoji_picker_ensure_css (void)
|
||||
{
|
||||
static gboolean done = FALSE;
|
||||
GtkCssProvider *provider;
|
||||
|
||||
if (done)
|
||||
return;
|
||||
done = TRUE;
|
||||
|
||||
provider = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (provider,
|
||||
".zc-emoji-cell {"
|
||||
" font-family: \"Noto Color Emoji\", \"Segoe UI Emoji\","
|
||||
" \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";"
|
||||
" font-size: 15pt;"
|
||||
" padding: 2px;"
|
||||
"}"
|
||||
".zc-emoji-cell-small {"
|
||||
" font-family: \"Noto Color Emoji\", \"Segoe UI Emoji\","
|
||||
" \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";"
|
||||
"}",
|
||||
-1, NULL);
|
||||
gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
|
||||
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||
g_object_unref (provider);
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
emoji_label_new (const char *sequence, gboolean small_cell)
|
||||
{
|
||||
GtkWidget *label = gtk_label_new (sequence);
|
||||
|
||||
gtk_style_context_add_class (gtk_widget_get_style_context (label),
|
||||
small_cell ? "zc-emoji-cell-small" : "zc-emoji-cell");
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_picker_set_a11y_name (GtkWidget *widget, const char *name)
|
||||
{
|
||||
AtkObject *accessible = gtk_widget_get_accessible (widget);
|
||||
|
||||
if (accessible)
|
||||
atk_object_set_name (accessible, name);
|
||||
}
|
||||
|
||||
static EmojiTone
|
||||
emoji_picker_current_tone (void)
|
||||
{
|
||||
int tone = prefs.hex_gui_emoji_skin_tone;
|
||||
|
||||
if (tone < EMOJI_TONE_NONE || tone >= EMOJI_TONE_COUNT)
|
||||
return EMOJI_TONE_NONE;
|
||||
|
||||
return (EmojiTone) tone;
|
||||
}
|
||||
|
||||
/* "waving hand" / "waving hand (Dark skin tone)", with :shortcodes: on a
|
||||
* second line - used for both tooltips and accessible names. */
|
||||
static char *
|
||||
emoji_picker_describe (const EmojiEntry *entry, EmojiTone tone)
|
||||
{
|
||||
GString *desc = g_string_new (NULL);
|
||||
char **aliases;
|
||||
gsize i;
|
||||
|
||||
if (tone != EMOJI_TONE_NONE && entry->tone_set >= 0)
|
||||
g_string_append_printf (desc, "%s (%s)", entry->name,
|
||||
emoji_data_tone_display_name (tone));
|
||||
else
|
||||
g_string_append (desc, entry->name);
|
||||
|
||||
if (*entry->aliases)
|
||||
{
|
||||
g_string_append_c (desc, '\n');
|
||||
aliases = g_strsplit (entry->aliases, " ", -1);
|
||||
for (i = 0; aliases[i]; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
g_string_append_c (desc, ' ');
|
||||
g_string_append_printf (desc, ":%s:", aliases[i]);
|
||||
}
|
||||
g_strfreev (aliases);
|
||||
}
|
||||
|
||||
return g_string_free (desc, FALSE);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Insertion
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
emoji_picker_insert (EmojiPicker *picker, const char *sequence)
|
||||
{
|
||||
GtkEditable *editable;
|
||||
int pos;
|
||||
|
||||
if (!picker->target || !sequence || !*sequence)
|
||||
return;
|
||||
|
||||
gtk_popover_popdown (GTK_POPOVER (picker->popover));
|
||||
|
||||
editable = GTK_EDITABLE (picker->target);
|
||||
gtk_editable_delete_selection (editable);
|
||||
pos = gtk_editable_get_position (editable);
|
||||
gtk_editable_insert_text (editable, sequence, strlen (sequence), &pos);
|
||||
|
||||
/* focus the entry without letting GtkEntry select everything */
|
||||
gtk_editable_set_editable (editable, FALSE);
|
||||
gtk_widget_grab_focus (picker->target);
|
||||
gtk_editable_set_editable (editable, TRUE);
|
||||
gtk_editable_set_position (editable, pos);
|
||||
|
||||
emoji_recents_add (sequence);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_picker_activate_child (EmojiPicker *picker, GtkFlowBoxChild *child)
|
||||
{
|
||||
emoji_picker_insert (picker,
|
||||
g_object_get_data (G_OBJECT (child), "emoji-sequence"));
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_grid_child_activated_cb (GtkFlowBox *grid, GtkFlowBoxChild *child,
|
||||
gpointer userdata)
|
||||
{
|
||||
emoji_picker_activate_child (userdata, child);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Grid contents
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
emoji_grid_add (EmojiPicker *picker, const EmojiEntry *entry,
|
||||
const char *sequence, EmojiTone tone)
|
||||
{
|
||||
GtkWidget *child, *label;
|
||||
char *desc;
|
||||
|
||||
label = emoji_label_new (sequence, FALSE);
|
||||
child = gtk_flow_box_child_new ();
|
||||
gtk_container_add (GTK_CONTAINER (child), label);
|
||||
g_object_set_data_full (G_OBJECT (child), "emoji-sequence",
|
||||
g_strdup (sequence), g_free);
|
||||
|
||||
desc = emoji_picker_describe (entry, tone);
|
||||
gtk_widget_set_tooltip_text (child, desc);
|
||||
emoji_picker_set_a11y_name (child, desc);
|
||||
g_free (desc);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (picker->grid), child);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_picker_refresh (EmojiPicker *picker)
|
||||
{
|
||||
const char *text = gtk_entry_get_text (GTK_ENTRY (picker->search_entry));
|
||||
EmojiTone tone = emoji_picker_current_tone ();
|
||||
GtkAdjustment *vadj;
|
||||
gboolean any = FALSE;
|
||||
gsize i, results = 0;
|
||||
|
||||
gtk_container_foreach (GTK_CONTAINER (picker->grid),
|
||||
(GtkCallback) gtk_widget_destroy, NULL);
|
||||
|
||||
if (text && *text)
|
||||
{
|
||||
char **tokens = emoji_search_tokenize (text);
|
||||
|
||||
if (tokens)
|
||||
{
|
||||
for (i = 0; i < emoji_data_count () &&
|
||||
results < EMOJI_SEARCH_MAX_RESULTS; i++)
|
||||
{
|
||||
const EmojiEntry *entry = emoji_data_entry (i);
|
||||
|
||||
if (!emoji_entry_matches (entry, tokens))
|
||||
continue;
|
||||
emoji_grid_add (picker, entry,
|
||||
emoji_entry_sequence_for_tone (entry, tone), tone);
|
||||
results++;
|
||||
}
|
||||
g_strfreev (tokens);
|
||||
any = results > 0;
|
||||
}
|
||||
gtk_label_set_text (GTK_LABEL (picker->empty_label),
|
||||
_("No emoji found"));
|
||||
}
|
||||
else if (picker->category == EMOJI_CATEGORY_RECENT)
|
||||
{
|
||||
emoji_recents_load ();
|
||||
for (i = 0; i < recent_emoji->len; i++)
|
||||
{
|
||||
const char *sequence = g_ptr_array_index (recent_emoji, i);
|
||||
EmojiTone seq_tone;
|
||||
const EmojiEntry *entry =
|
||||
emoji_data_find_by_sequence (sequence, &seq_tone);
|
||||
|
||||
if (!entry)
|
||||
continue;
|
||||
emoji_grid_add (picker, entry, sequence, seq_tone);
|
||||
any = TRUE;
|
||||
}
|
||||
gtk_label_set_text (GTK_LABEL (picker->empty_label),
|
||||
_("No recently used emoji"));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (i = 0; i < emoji_data_count (); i++)
|
||||
{
|
||||
const EmojiEntry *entry = emoji_data_entry (i);
|
||||
|
||||
if (entry->group != picker->category)
|
||||
continue;
|
||||
emoji_grid_add (picker, entry,
|
||||
emoji_entry_sequence_for_tone (entry, tone), tone);
|
||||
any = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
gtk_widget_show_all (picker->grid);
|
||||
gtk_stack_set_visible_child_name (GTK_STACK (picker->stack),
|
||||
any ? "grid" : "empty");
|
||||
|
||||
vadj = gtk_scrolled_window_get_vadjustment (
|
||||
GTK_SCROLLED_WINDOW (picker->scroller));
|
||||
gtk_adjustment_set_value (vadj, gtk_adjustment_get_lower (vadj));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Categories
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
emoji_picker_sync_category_buttons (EmojiPicker *picker)
|
||||
{
|
||||
gsize i;
|
||||
|
||||
picker->updating_buttons = TRUE;
|
||||
for (i = 0; i < G_N_ELEMENTS (picker->category_buttons); i++)
|
||||
{
|
||||
int category = (int) i - 1;
|
||||
|
||||
gtk_toggle_button_set_active (
|
||||
GTK_TOGGLE_BUTTON (picker->category_buttons[i]),
|
||||
category == picker->category);
|
||||
}
|
||||
picker->updating_buttons = FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_category_toggled_cb (GtkToggleButton *button, gpointer userdata)
|
||||
{
|
||||
EmojiPicker *picker = userdata;
|
||||
int category = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
|
||||
"emoji-category"));
|
||||
|
||||
if (picker->updating_buttons)
|
||||
return;
|
||||
|
||||
if (!gtk_toggle_button_get_active (button))
|
||||
{
|
||||
/* the active category cannot be untoggled */
|
||||
if (picker->category == category)
|
||||
emoji_picker_sync_category_buttons (picker);
|
||||
return;
|
||||
}
|
||||
|
||||
picker->category = category;
|
||||
emoji_picker_sync_category_buttons (picker);
|
||||
|
||||
/* leave search mode; clearing the entry triggers the refresh */
|
||||
if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry)))
|
||||
gtk_entry_set_text (GTK_ENTRY (picker->search_entry), "");
|
||||
else
|
||||
emoji_picker_refresh (picker);
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
emoji_category_button_new (EmojiPicker *picker, int category,
|
||||
const char *icon_sequence, const char *name)
|
||||
{
|
||||
GtkWidget *button = gtk_toggle_button_new ();
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (button),
|
||||
emoji_label_new (icon_sequence, TRUE));
|
||||
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
|
||||
gtk_widget_set_tooltip_text (button, name);
|
||||
emoji_picker_set_a11y_name (button, name);
|
||||
g_object_set_data (G_OBJECT (button), "emoji-category",
|
||||
GINT_TO_POINTER (category));
|
||||
g_signal_connect (G_OBJECT (button), "toggled",
|
||||
G_CALLBACK (emoji_category_toggled_cb), picker);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Skin tones
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
#define EMOJI_TONE_SAMPLE "\342\234\213" /* ✋ raised hand */
|
||||
|
||||
static void
|
||||
emoji_tone_toggled_cb (GtkToggleButton *button, gpointer userdata)
|
||||
{
|
||||
EmojiPicker *picker = userdata;
|
||||
int tone = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
|
||||
"emoji-tone"));
|
||||
|
||||
if (!gtk_toggle_button_get_active (button))
|
||||
return;
|
||||
if (prefs.hex_gui_emoji_skin_tone == tone)
|
||||
return;
|
||||
|
||||
prefs.hex_gui_emoji_skin_tone = tone;
|
||||
preferences_persistence_save_all ();
|
||||
emoji_picker_refresh (picker);
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
emoji_tone_row_new (EmojiPicker *picker)
|
||||
{
|
||||
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
GtkWidget *button;
|
||||
GSList *group = NULL;
|
||||
const EmojiEntry *sample = emoji_data_find_by_sequence (EMOJI_TONE_SAMPLE,
|
||||
NULL);
|
||||
int tone;
|
||||
|
||||
gtk_widget_set_halign (box, GTK_ALIGN_CENTER);
|
||||
|
||||
for (tone = EMOJI_TONE_NONE; tone < EMOJI_TONE_COUNT; tone++)
|
||||
{
|
||||
const char *sequence = EMOJI_TONE_SAMPLE;
|
||||
|
||||
if (sample)
|
||||
sequence = emoji_entry_sequence_for_tone (sample, tone);
|
||||
|
||||
button = gtk_radio_button_new (group);
|
||||
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button));
|
||||
gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (button), FALSE);
|
||||
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
|
||||
gtk_container_add (GTK_CONTAINER (button),
|
||||
emoji_label_new (sequence, TRUE));
|
||||
gtk_widget_set_tooltip_text (button,
|
||||
emoji_data_tone_display_name (tone));
|
||||
emoji_picker_set_a11y_name (button,
|
||||
emoji_data_tone_display_name (tone));
|
||||
g_object_set_data (G_OBJECT (button), "emoji-tone",
|
||||
GINT_TO_POINTER (tone));
|
||||
picker->tone_buttons[tone] = button;
|
||||
|
||||
if (emoji_picker_current_tone () == tone)
|
||||
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
|
||||
|
||||
g_signal_connect (G_OBJECT (button), "toggled",
|
||||
G_CALLBACK (emoji_tone_toggled_cb), picker);
|
||||
|
||||
gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
|
||||
}
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
/* the pref can also change externally (/set gui_emoji_skin_tone) */
|
||||
static void
|
||||
emoji_picker_sync_tone_buttons (EmojiPicker *picker)
|
||||
{
|
||||
EmojiTone tone = emoji_picker_current_tone ();
|
||||
|
||||
gtk_toggle_button_set_active (
|
||||
GTK_TOGGLE_BUTTON (picker->tone_buttons[tone]), TRUE);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Search
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
emoji_search_changed_cb (GtkSearchEntry *entry, gpointer userdata)
|
||||
{
|
||||
emoji_picker_refresh (userdata);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_search_activate_cb (GtkEntry *entry, gpointer userdata)
|
||||
{
|
||||
EmojiPicker *picker = userdata;
|
||||
GtkFlowBoxChild *child =
|
||||
gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0);
|
||||
|
||||
/* Enter inserts the first hit */
|
||||
if (child)
|
||||
emoji_picker_activate_child (picker, child);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
emoji_search_key_press_cb (GtkWidget *entry, GdkEventKey *event,
|
||||
gpointer userdata)
|
||||
{
|
||||
EmojiPicker *picker = userdata;
|
||||
GtkFlowBoxChild *child;
|
||||
|
||||
if (event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down)
|
||||
{
|
||||
child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0);
|
||||
if (child)
|
||||
{
|
||||
gtk_widget_grab_focus (GTK_WIDGET (child));
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ *
|
||||
* Popover assembly
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
emoji_picker_build_contents (EmojiPicker *picker)
|
||||
{
|
||||
GtkWidget *vbox, *categories, *button;
|
||||
int group;
|
||||
|
||||
emoji_picker_ensure_css ();
|
||||
|
||||
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
|
||||
g_object_set (vbox, "margin", 6, NULL);
|
||||
|
||||
picker->search_entry = gtk_search_entry_new ();
|
||||
gtk_entry_set_placeholder_text (GTK_ENTRY (picker->search_entry),
|
||||
_("Search emoji"));
|
||||
emoji_picker_set_a11y_name (picker->search_entry, _("Search emoji"));
|
||||
g_signal_connect (G_OBJECT (picker->search_entry), "search-changed",
|
||||
G_CALLBACK (emoji_search_changed_cb), picker);
|
||||
g_signal_connect (G_OBJECT (picker->search_entry), "activate",
|
||||
G_CALLBACK (emoji_search_activate_cb), picker);
|
||||
g_signal_connect (G_OBJECT (picker->search_entry), "key-press-event",
|
||||
G_CALLBACK (emoji_search_key_press_cb), picker);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), picker->search_entry, FALSE, FALSE, 0);
|
||||
|
||||
categories = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_box_set_homogeneous (GTK_BOX (categories), TRUE);
|
||||
|
||||
picker->category_buttons[0] = emoji_category_button_new (picker,
|
||||
EMOJI_CATEGORY_RECENT, "\360\237\225\230" /* 🕘 */,
|
||||
_("Recently Used"));
|
||||
gtk_box_pack_start (GTK_BOX (categories), picker->category_buttons[0],
|
||||
TRUE, TRUE, 0);
|
||||
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
|
||||
{
|
||||
button = emoji_category_button_new (picker, group,
|
||||
emoji_data_group_icon_sequence (group),
|
||||
emoji_data_group_display_name (group));
|
||||
picker->category_buttons[group + 1] = button;
|
||||
gtk_box_pack_start (GTK_BOX (categories), button, TRUE, TRUE, 0);
|
||||
}
|
||||
gtk_box_pack_start (GTK_BOX (vbox), categories, FALSE, FALSE, 0);
|
||||
|
||||
picker->grid = gtk_flow_box_new ();
|
||||
gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (picker->grid),
|
||||
GTK_SELECTION_NONE);
|
||||
gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (picker->grid),
|
||||
TRUE);
|
||||
gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (picker->grid),
|
||||
EMOJI_GRID_COLUMNS);
|
||||
gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (picker->grid),
|
||||
EMOJI_GRID_COLUMNS);
|
||||
gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (picker->grid), TRUE);
|
||||
gtk_widget_set_valign (picker->grid, GTK_ALIGN_START);
|
||||
g_signal_connect (G_OBJECT (picker->grid), "child-activated",
|
||||
G_CALLBACK (emoji_grid_child_activated_cb), picker);
|
||||
|
||||
picker->scroller = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (picker->scroller),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_min_content_height (
|
||||
GTK_SCROLLED_WINDOW (picker->scroller), 300);
|
||||
gtk_container_add (GTK_CONTAINER (picker->scroller), picker->grid);
|
||||
|
||||
picker->empty_label = gtk_label_new (_("No emoji found"));
|
||||
gtk_style_context_add_class (
|
||||
gtk_widget_get_style_context (picker->empty_label), "dim-label");
|
||||
|
||||
picker->stack = gtk_stack_new ();
|
||||
gtk_widget_set_size_request (picker->stack, 380, -1);
|
||||
gtk_stack_add_named (GTK_STACK (picker->stack), picker->scroller, "grid");
|
||||
gtk_stack_add_named (GTK_STACK (picker->stack), picker->empty_label,
|
||||
"empty");
|
||||
gtk_box_pack_start (GTK_BOX (vbox), picker->stack, TRUE, TRUE, 0);
|
||||
|
||||
gtk_box_pack_start (GTK_BOX (vbox), emoji_tone_row_new (picker),
|
||||
FALSE, FALSE, 0);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (picker->popover), vbox);
|
||||
gtk_widget_show_all (vbox);
|
||||
|
||||
emoji_picker_sync_category_buttons (picker);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_popover_show_cb (GtkWidget *popover, gpointer userdata)
|
||||
{
|
||||
EmojiPicker *picker = userdata;
|
||||
|
||||
if (!picker->populated)
|
||||
{
|
||||
picker->populated = TRUE;
|
||||
emoji_picker_build_contents (picker);
|
||||
}
|
||||
else if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry)))
|
||||
{
|
||||
gtk_entry_set_text (GTK_ENTRY (picker->search_entry), "");
|
||||
}
|
||||
|
||||
emoji_picker_sync_tone_buttons (picker);
|
||||
emoji_picker_refresh (picker);
|
||||
gtk_widget_grab_focus (picker->search_entry);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_popover_closed_cb (GtkWidget *popover, gpointer userdata)
|
||||
{
|
||||
EmojiPicker *picker = userdata;
|
||||
GtkEditable *editable;
|
||||
|
||||
if (!picker->target)
|
||||
return;
|
||||
|
||||
/* hand focus back to the input box without selecting its contents */
|
||||
editable = GTK_EDITABLE (picker->target);
|
||||
gtk_editable_set_editable (editable, FALSE);
|
||||
gtk_widget_grab_focus (picker->target);
|
||||
gtk_editable_set_editable (editable, TRUE);
|
||||
}
|
||||
|
||||
static const char *
|
||||
emoji_picker_button_icon_name (void)
|
||||
{
|
||||
static const char *icon_names[] = {
|
||||
"face-smile-symbolic",
|
||||
"face-smile",
|
||||
"insert-emoticon-symbolic",
|
||||
"insert-emoticon",
|
||||
"zc-menu-emoji",
|
||||
};
|
||||
GtkIconTheme *theme = gtk_icon_theme_get_default ();
|
||||
gsize i;
|
||||
|
||||
if (!theme)
|
||||
return NULL;
|
||||
|
||||
for (i = 0; i < G_N_ELEMENTS (icon_names); i++)
|
||||
{
|
||||
if (gtk_icon_theme_has_icon (theme, icon_names[i]))
|
||||
return icon_names[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_picker_free (gpointer data)
|
||||
{
|
||||
EmojiPicker *picker = data;
|
||||
|
||||
if (picker->target)
|
||||
g_object_remove_weak_pointer (G_OBJECT (picker->target),
|
||||
(gpointer *) &picker->target);
|
||||
g_free (picker);
|
||||
}
|
||||
|
||||
GtkWidget *
|
||||
emoji_picker_button_new (GtkWidget *target_entry)
|
||||
{
|
||||
EmojiPicker *picker;
|
||||
const char *icon_name;
|
||||
|
||||
g_return_val_if_fail (GTK_IS_ENTRY (target_entry), NULL);
|
||||
|
||||
picker = g_new0 (EmojiPicker, 1);
|
||||
picker->target = target_entry;
|
||||
picker->category = EMOJI_GROUP_SMILEYS_EMOTION;
|
||||
g_object_add_weak_pointer (G_OBJECT (target_entry),
|
||||
(gpointer *) &picker->target);
|
||||
|
||||
picker->button = gtk_menu_button_new ();
|
||||
gtk_button_set_relief (GTK_BUTTON (picker->button), GTK_RELIEF_NONE);
|
||||
gtk_widget_set_can_focus (picker->button, FALSE);
|
||||
gtk_widget_set_tooltip_text (picker->button, _("Insert Emoji"));
|
||||
emoji_picker_set_a11y_name (picker->button, _("Insert Emoji"));
|
||||
g_object_set_data_full (G_OBJECT (picker->button), "emoji-picker",
|
||||
picker, emoji_picker_free);
|
||||
|
||||
icon_name = emoji_picker_button_icon_name ();
|
||||
if (icon_name)
|
||||
{
|
||||
gtk_button_set_image (GTK_BUTTON (picker->button),
|
||||
gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU));
|
||||
}
|
||||
else
|
||||
{
|
||||
/* no themed icon anywhere: fall back to an emoji label so the
|
||||
* button never turns into an unusable placeholder */
|
||||
emoji_picker_ensure_css ();
|
||||
gtk_container_add (GTK_CONTAINER (picker->button),
|
||||
emoji_label_new ("\360\237\231\202" /* 🙂 */, TRUE));
|
||||
}
|
||||
|
||||
/* the popover shell is cheap; its contents are built on first open so
|
||||
* startup cost stays unchanged */
|
||||
picker->popover = gtk_popover_new (picker->button);
|
||||
gtk_popover_set_position (GTK_POPOVER (picker->popover), GTK_POS_TOP);
|
||||
g_signal_connect (G_OBJECT (picker->popover), "show",
|
||||
G_CALLBACK (emoji_popover_show_cb), picker);
|
||||
g_signal_connect (G_OBJECT (picker->popover), "closed",
|
||||
G_CALLBACK (emoji_popover_closed_cb), picker);
|
||||
gtk_menu_button_set_popover (GTK_MENU_BUTTON (picker->button),
|
||||
picker->popover);
|
||||
|
||||
return picker->button;
|
||||
}
|
||||
31
src/fe-gtk/emoji-picker.h
Normal file
31
src/fe-gtk/emoji-picker.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/* ZoiteChat
|
||||
* Copyright (C) 2026 deepend-tildeclub.
|
||||
*
|
||||
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
#ifndef ZOITECHAT_EMOJI_PICKER_H
|
||||
#define ZOITECHAT_EMOJI_PICKER_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/* Creates the emoji picker button for an input entry. The returned
|
||||
* GtkMenuButton opens a popover with search, categories, recently used
|
||||
* emoji and skin-tone selection; activating an emoji inserts its Unicode
|
||||
* sequence into target_entry at the cursor. The popover contents are
|
||||
* built lazily on first open. */
|
||||
GtkWidget *emoji_picker_button_new (GtkWidget *target_entry);
|
||||
|
||||
#endif
|
||||
@@ -102,6 +102,86 @@ static const GOptionEntry gopt_entries[] =
|
||||
{NULL}
|
||||
};
|
||||
|
||||
/* The emoji font is only bundled with Windows builds; other platforms
|
||||
* rely on the operating system's emoji font (e.g. the distribution's
|
||||
* Noto Color Emoji package on Linux). */
|
||||
#ifdef WIN32
|
||||
static gunichar2 *win32_private_emoji_font;
|
||||
|
||||
static char *
|
||||
fe_bundled_emoji_font_path (void)
|
||||
{
|
||||
const char *override_path;
|
||||
char *font_path = NULL;
|
||||
|
||||
override_path = g_getenv ("ZOITECHAT_EMOJI_FONT");
|
||||
if (override_path && *override_path)
|
||||
font_path = g_strdup (override_path);
|
||||
else
|
||||
{
|
||||
char *base_path;
|
||||
|
||||
base_path = g_win32_get_package_installation_directory_of_module (NULL);
|
||||
if (base_path)
|
||||
{
|
||||
font_path = g_build_filename (base_path, "share", "fonts",
|
||||
"zoitechat", "NotoColorEmoji.ttf", NULL);
|
||||
g_free (base_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (font_path && !g_file_test (font_path, G_FILE_TEST_IS_REGULAR))
|
||||
{
|
||||
g_free (font_path);
|
||||
font_path = NULL;
|
||||
}
|
||||
|
||||
return font_path;
|
||||
}
|
||||
|
||||
static void
|
||||
fe_register_bundled_emoji_font (void)
|
||||
{
|
||||
char *font_path;
|
||||
|
||||
/* Windows 7 cannot render color emoji fonts, so the bundled Noto
|
||||
* Color Emoji font is not registered there; emoji rendering falls
|
||||
* back to the monochrome Segoe UI Symbol glyphs instead. */
|
||||
if (!win32_is_windows_8_or_newer ())
|
||||
{
|
||||
g_debug ("Windows 7 detected; using Segoe UI Symbol for emoji");
|
||||
return;
|
||||
}
|
||||
|
||||
font_path = fe_bundled_emoji_font_path ();
|
||||
if (!font_path)
|
||||
{
|
||||
g_debug ("Bundled Noto Color Emoji font was not found; using platform font fallback");
|
||||
return;
|
||||
}
|
||||
|
||||
win32_private_emoji_font = g_utf8_to_utf16 (font_path, -1, NULL, NULL, NULL);
|
||||
if (!win32_private_emoji_font ||
|
||||
AddFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL) == 0)
|
||||
{
|
||||
g_warning ("Unable to register bundled emoji font: %s", font_path);
|
||||
g_clear_pointer (&win32_private_emoji_font, g_free);
|
||||
}
|
||||
|
||||
g_free (font_path);
|
||||
}
|
||||
|
||||
static void
|
||||
fe_unregister_bundled_emoji_font (void)
|
||||
{
|
||||
if (win32_private_emoji_font)
|
||||
{
|
||||
RemoveFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL);
|
||||
g_clear_pointer (&win32_private_emoji_font, g_free);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
static void
|
||||
create_msg_dialog (gchar *title, gchar *message)
|
||||
@@ -124,6 +204,25 @@ create_msg_dialog (gchar *title, gchar *message)
|
||||
|
||||
static char *win32_argv0_dir;
|
||||
|
||||
static void
|
||||
win32_set_appusermodelid (void)
|
||||
{
|
||||
HMODULE shell32;
|
||||
HRESULT (WINAPI *set_appid) (PCWSTR);
|
||||
|
||||
shell32 = GetModuleHandleW (L"shell32.dll");
|
||||
if (!shell32)
|
||||
shell32 = LoadLibraryW (L"shell32.dll");
|
||||
if (!shell32)
|
||||
return;
|
||||
|
||||
set_appid = (HRESULT (WINAPI *) (PCWSTR)) GetProcAddress (shell32, "SetCurrentProcessExplicitAppUserModelID");
|
||||
if (!set_appid)
|
||||
return;
|
||||
|
||||
set_appid (L"ZoiteChat.Desktop.Notify");
|
||||
}
|
||||
|
||||
static void
|
||||
win32_set_gsettings_schema_dir (void)
|
||||
{
|
||||
@@ -421,7 +520,10 @@ fe_args (int argc, char *argv[])
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
fe_register_bundled_emoji_font ();
|
||||
|
||||
win32_set_gsettings_schema_dir ();
|
||||
win32_set_appusermodelid ();
|
||||
win32_configure_pixbuf_loaders ();
|
||||
|
||||
/* this is mainly for irc:// URL handling. When windows calls us from */
|
||||
@@ -594,6 +696,9 @@ fe_main (void)
|
||||
#endif
|
||||
|
||||
gtk_main ();
|
||||
#ifdef WIN32
|
||||
fe_unregister_bundled_emoji_font ();
|
||||
#endif
|
||||
|
||||
/* sleep for 2 seconds so any QUIT messages are not lost. The */
|
||||
/* GUI is closed at this point, so the user doesn't even know! */
|
||||
@@ -792,18 +897,19 @@ fe_set_topic (session *sess, char *topic, char *stripped_topic)
|
||||
{
|
||||
if (!sess->gui->is_tab || sess == current_tab)
|
||||
{
|
||||
GtkTextBuffer *topic_buffer;
|
||||
GtkTextIter start;
|
||||
|
||||
topic_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (sess->gui->topic_entry));
|
||||
if (prefs.hex_text_stripcolor_topic)
|
||||
{
|
||||
gtk_text_buffer_set_text (
|
||||
gtk_text_view_get_buffer (GTK_TEXT_VIEW (sess->gui->topic_entry)),
|
||||
stripped_topic, -1);
|
||||
}
|
||||
gtk_text_buffer_set_text (topic_buffer, stripped_topic, -1);
|
||||
else
|
||||
{
|
||||
gtk_text_buffer_set_text (
|
||||
gtk_text_view_get_buffer (GTK_TEXT_VIEW (sess->gui->topic_entry)),
|
||||
topic, -1);
|
||||
}
|
||||
gtk_text_buffer_set_text (topic_buffer, topic, -1);
|
||||
|
||||
gtk_text_buffer_get_start_iter (topic_buffer, &start);
|
||||
gtk_text_buffer_place_cursor (topic_buffer, &start);
|
||||
gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (sess->gui->topic_entry),
|
||||
&start, 0.0, FALSE, 0.0, 0.0);
|
||||
mg_set_topic_tip (sess);
|
||||
}
|
||||
else
|
||||
@@ -1336,17 +1442,19 @@ fe_open_url_inner (const char *url)
|
||||
gboolean opened = FALSE;
|
||||
|
||||
#ifdef WIN32
|
||||
opened = g_app_info_launch_default_for_uri (escaped_url, NULL, &error);
|
||||
gunichar2 *url_utf16 = g_utf8_to_utf16 (escaped_url, -1, NULL, NULL, NULL);
|
||||
|
||||
if (url_utf16 != NULL)
|
||||
{
|
||||
opened = ((INT_PTR) ShellExecuteW (0, L"open", url_utf16, NULL, NULL, SW_SHOWNORMAL)) > 32;
|
||||
g_free (url_utf16);
|
||||
}
|
||||
|
||||
if (!opened)
|
||||
{
|
||||
g_clear_error (&error);
|
||||
gunichar2 *url_utf16 = g_utf8_to_utf16 (escaped_url, -1, NULL, NULL, NULL);
|
||||
|
||||
if (url_utf16 != NULL)
|
||||
{
|
||||
opened = ((INT_PTR) ShellExecuteW (0, L"open", url_utf16, NULL, NULL, SW_SHOWNORMAL)) > 32;
|
||||
g_free (url_utf16);
|
||||
}
|
||||
opened = g_app_info_launch_default_for_uri (escaped_url, NULL, &error);
|
||||
if (!opened)
|
||||
g_clear_error (&error);
|
||||
}
|
||||
#else
|
||||
{
|
||||
|
||||
@@ -163,6 +163,8 @@ typedef struct session_gui
|
||||
*op_xpm, /* icon to the left of nickname */
|
||||
*namelistinfo, /* label above userlist */
|
||||
*input_box,
|
||||
*reply_box,
|
||||
*reply_label,
|
||||
*flag_wid[NUM_FLAG_WIDS], /* channelmode buttons */
|
||||
*limit_entry, /* +l */
|
||||
*key_entry; /* +k */
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
@@ -97,6 +97,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
||||
<ClCompile Include="custom-list.c" />
|
||||
<ClCompile Include="dccgui.c" />
|
||||
<ClCompile Include="editlist.c" />
|
||||
<ClCompile Include="emoji-picker.c" />
|
||||
<ClCompile Include="fe-gtk.c" />
|
||||
<ClCompile Include="fkeys.c" />
|
||||
<ClCompile Include="gtkutil.c" />
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
#define ICON_FKEYS_DELETE "edit-delete"
|
||||
#define ICON_FKEYS_CANCEL "dialog-cancel"
|
||||
#define ICON_FKEYS_SAVE "document-save"
|
||||
#define ICON_FKEYS_RESET "edit-undo"
|
||||
|
||||
static void replace_handle (GtkWidget * wid);
|
||||
void key_check_replace_on_change (GtkEditable *editable, gpointer data);
|
||||
@@ -80,7 +81,7 @@ void key_action_tab_clean (void);
|
||||
*/
|
||||
|
||||
/* Remember that the *number* of actions is this *plus* 1 --AGL */
|
||||
#define KEY_MAX_ACTIONS 14
|
||||
#define KEY_MAX_ACTIONS 16
|
||||
|
||||
struct key_binding
|
||||
{
|
||||
@@ -105,7 +106,10 @@ struct gcomp_data
|
||||
};
|
||||
|
||||
static int key_load_kbs (void);
|
||||
static int key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list);
|
||||
static int key_save_kbs (void);
|
||||
static void key_dialog_load (GtkListStore *store);
|
||||
static void key_dialog_reset (GtkWidget *wid, gpointer userdata);
|
||||
static int key_action_handle_command (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
@@ -142,6 +146,13 @@ static int key_action_move_tab_family_right (GtkWidget * wid, GdkEventKey * evt,
|
||||
static int key_action_put_history (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
static int key_action_menu_shortcut (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
static int key_action_reopen_closed_tab (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
|
||||
|
||||
static GSList *keybind_list = NULL;
|
||||
|
||||
@@ -260,7 +271,7 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
|
||||
{key_action_handle_command, "Run Command",
|
||||
N_("The \002Run Command\002 action runs the data in Data 1 as if it had been typed into the entry box where you pressed the key sequence. Thus it can contain text (which will be sent to the channel/person), commands or user commands. Escapes in Data 1 are interpreted (for example \002\\n\002, \002\\r\002, \002\\t\002, \002\\xNN\002 and \002\\\\\002), so it is possible to run more than one command by using newlines.")},
|
||||
{key_action_page_switch, "Change Page",
|
||||
N_("The \002Change Page\002 command switches between pages in the notebook. Set Data 1 to the page you want to switch to. If Data 2 is set to anything then the switch will be relative to the current position. Set Data 1 to auto to switch to the page with the most recent and important activity (queries first, then channels with hilight, channels with dialogue, channels with other data)")},
|
||||
N_("The \002Change Page\002 command switches between pages in the notebook. Set Data 1 to the page number, channel name, network/channel, or auto. If Data 2 is set to anything then numeric switches are relative to the current position. Auto switches to the page with the most recent and important activity (queries first, then channels with hilight, channels with dialogue, channels with other data)")},
|
||||
{key_action_insert, "Insert in Buffer",
|
||||
N_("The \002Insert in Buffer\002 command will insert the contents of Data 1 into the entry where the key sequence was pressed at the current cursor position. Escapes in Data 1 are interpreted (for example \002\\n\002, \002\\r\002, \002\\t\002, \002\\xNN\002 and \002\\\\\002).")},
|
||||
{key_action_scroll_page, "Scroll Page",
|
||||
@@ -287,6 +298,10 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
|
||||
N_("This command moves the current tab family to the right")},
|
||||
{key_action_put_history, "Push input line into history",
|
||||
N_("Push input line into history but doesn't send to server")},
|
||||
{key_action_menu_shortcut, "Menu Shortcut",
|
||||
N_("Runs one of the built-in menu shortcuts. Set Data 1 to: network-list, new-server-tab, new-server-window, close, quit, menu-toggle, user-list-toggle, fullscreen-toggle, away-toggle, reset-marker, move-marker, copy-selection, search-text, search-next, search-previous or contents.")},
|
||||
{key_action_reopen_closed_tab, "Reopen Closed Tab",
|
||||
N_("Reopens the most recently closed channel tab")},
|
||||
};
|
||||
|
||||
#define default_kb_cfg \
|
||||
@@ -329,7 +344,86 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
|
||||
"ACCEL=<Alt>Right\nMove front tab right\nD1!\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>Page_Up\nMove tab family left\nD1!\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>Page_Down\nMove tab family right\nD1!\nD2!\n\n"\
|
||||
"ACCEL=F9\nRun Command\nD1:/GUI MENU TOGGLE\nD2!\n\n"
|
||||
"ACCEL=<Primary>s\nMenu Shortcut\nD1:network-list\nD2!\n\n"\
|
||||
"ACCEL=<Primary>t\nMenu Shortcut\nD1:new-server-tab\nD2!\n\n"\
|
||||
"ACCEL=<Primary>n\nMenu Shortcut\nD1:new-server-window\nD2!\n\n"\
|
||||
"ACCEL=<Primary>w\nMenu Shortcut\nD1:close\nD2!\n\n"\
|
||||
"ACCEL=<Primary>q\nMenu Shortcut\nD1:quit\nD2!\n\n"\
|
||||
"ACCEL=<Primary>F9\nMenu Shortcut\nD1:menu-toggle\nD2!\n\n"\
|
||||
"ACCEL=F7\nMenu Shortcut\nD1:user-list-toggle\nD2!\n\n"\
|
||||
"ACCEL=F11\nMenu Shortcut\nD1:fullscreen-toggle\nD2!\n\n"\
|
||||
"ACCEL=<Alt>a\nMenu Shortcut\nD1:away-toggle\nD2!\n\n"\
|
||||
"ACCEL=<Primary>m\nMenu Shortcut\nD1:reset-marker\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>M\nMenu Shortcut\nD1:move-marker\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>C\nMenu Shortcut\nD1:copy-selection\nD2!\n\n"\
|
||||
"ACCEL=<Primary>f\nMenu Shortcut\nD1:search-text\nD2!\n\n"\
|
||||
"ACCEL=<Primary>g\nMenu Shortcut\nD1:search-next\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>G\nMenu Shortcut\nD1:search-previous\nD2!\n\n"\
|
||||
"ACCEL=F1\nMenu Shortcut\nD1:contents\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>T\nReopen Closed Tab\nD1!\nD2!\n\n"
|
||||
|
||||
static gboolean
|
||||
key_builtin_data_match (char *line, char *data)
|
||||
{
|
||||
if (line[2] == '!')
|
||||
return data == NULL || data[0] == 0;
|
||||
|
||||
if (line[2] == ':')
|
||||
return !strcmp (&line[3], data ? data : "");
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
key_binding_is_builtin (struct key_binding *kb)
|
||||
{
|
||||
char *buf, *ibuf;
|
||||
char *action;
|
||||
int pnt = 0;
|
||||
int state = 0;
|
||||
gboolean match = FALSE;
|
||||
gboolean d1_match = FALSE;
|
||||
off_t size;
|
||||
|
||||
if (kb->action < 0 || kb->action > KEY_MAX_ACTIONS)
|
||||
return FALSE;
|
||||
|
||||
action = key_actions[kb->action].name;
|
||||
ibuf = g_strdup (default_kb_cfg);
|
||||
size = strlen (default_kb_cfg);
|
||||
|
||||
while (buf_get_line (ibuf, &buf, &pnt, size))
|
||||
{
|
||||
if (strlen (buf) == 0)
|
||||
continue;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
state = 1;
|
||||
break;
|
||||
case 1:
|
||||
match = !strcmp (buf, action);
|
||||
state = 2;
|
||||
break;
|
||||
case 2:
|
||||
d1_match = match && key_builtin_data_match (buf, kb->data1);
|
||||
state = 3;
|
||||
break;
|
||||
case 3:
|
||||
if (d1_match && key_builtin_data_match (buf, kb->data2))
|
||||
{
|
||||
g_free (ibuf);
|
||||
return TRUE;
|
||||
}
|
||||
state = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_free (ibuf);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
key_init ()
|
||||
@@ -443,7 +537,6 @@ key_handle_key_press (GtkWidget *wid, GdkEventKey *evt, session *sess)
|
||||
if (!list)
|
||||
return FALSE;
|
||||
current_sess = sess;
|
||||
|
||||
if (plugin_emit_keypress (sess, evt->state, evt->keyval, gdk_keyval_to_unicode (evt->keyval)))
|
||||
return 1;
|
||||
|
||||
@@ -487,6 +580,31 @@ key_handle_key_press (GtkWidget *wid, GdkEventKey *evt, session *sess)
|
||||
return 0;
|
||||
}
|
||||
|
||||
gboolean
|
||||
key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod)
|
||||
{
|
||||
struct key_binding *kb;
|
||||
GSList *list;
|
||||
|
||||
if (!name)
|
||||
return FALSE;
|
||||
|
||||
list = keybind_list;
|
||||
while (list)
|
||||
{
|
||||
kb = (struct key_binding*)list->data;
|
||||
if (kb->action >= 0 && kb->action <= KEY_MAX_ACTIONS && kb->keyval != 0 && !strcmp (key_actions[kb->action].name, "Menu Shortcut") && kb->data1 && !strcmp (kb->data1, name))
|
||||
{
|
||||
*keyval = kb->keyval;
|
||||
*mod = kb->mod;
|
||||
return TRUE;
|
||||
}
|
||||
list = g_slist_next (list);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
/* ***** GUI code here ******************* */
|
||||
|
||||
@@ -497,6 +615,7 @@ enum
|
||||
ACTION_COLUMN,
|
||||
D1_COLUMN,
|
||||
D2_COLUMN,
|
||||
CUSTOM_COLUMN,
|
||||
N_COLUMNS
|
||||
};
|
||||
|
||||
@@ -621,16 +740,32 @@ key_dialog_keypress (GtkWidget *wid, GdkEventKey *evt, gpointer userdata)
|
||||
|
||||
if (handled)
|
||||
{
|
||||
gboolean custom1, custom2;
|
||||
|
||||
sel = gtk_tree_view_get_selection (view);
|
||||
gtk_tree_selection_get_selected (sel, &store, &iter1);
|
||||
if (!gtk_tree_selection_get_selected (sel, &store, &iter1))
|
||||
return FALSE;
|
||||
|
||||
path = gtk_tree_model_get_path (store, &iter1);
|
||||
if (delta == 1)
|
||||
gtk_tree_path_next (path);
|
||||
else
|
||||
gtk_tree_path_prev (path);
|
||||
gtk_tree_model_get_iter (store, &iter2, path);
|
||||
else if (!gtk_tree_path_prev (path))
|
||||
{
|
||||
gtk_tree_path_free (path);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!gtk_tree_model_get_iter (store, &iter2, path))
|
||||
{
|
||||
gtk_tree_path_free (path);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gtk_tree_path_free (path);
|
||||
gtk_list_store_swap (GTK_LIST_STORE (store), &iter1, &iter2);
|
||||
gtk_tree_model_get (store, &iter1, CUSTOM_COLUMN, &custom1, -1);
|
||||
gtk_tree_model_get (store, &iter2, CUSTOM_COLUMN, &custom2, -1);
|
||||
if (custom1 && custom2)
|
||||
gtk_list_store_swap (GTK_LIST_STORE (store), &iter1, &iter2);
|
||||
}
|
||||
|
||||
return handled;
|
||||
@@ -642,14 +777,23 @@ key_dialog_selection_changed (GtkTreeSelection *sel, gpointer userdata)
|
||||
GtkTreeModel *model;
|
||||
GtkTreeIter iter;
|
||||
GtkXText *xtext;
|
||||
GtkWidget *delete_button;
|
||||
char *actiontext;
|
||||
gboolean custom;
|
||||
int action;
|
||||
|
||||
delete_button = g_object_get_data (G_OBJECT (key_dialog), "delete_button");
|
||||
if (!gtk_tree_selection_get_selected (sel, &model, &iter) || model == NULL)
|
||||
{
|
||||
if (delete_button)
|
||||
gtk_widget_set_sensitive (delete_button, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
xtext = GTK_XTEXT (g_object_get_data (G_OBJECT (key_dialog), "xtext"));
|
||||
gtk_tree_model_get (model, &iter, ACTION_COLUMN, &actiontext, -1);
|
||||
gtk_tree_model_get (model, &iter, ACTION_COLUMN, &actiontext, CUSTOM_COLUMN, &custom, -1);
|
||||
if (delete_button)
|
||||
gtk_widget_set_sensitive (delete_button, custom);
|
||||
|
||||
if (actiontext)
|
||||
{
|
||||
@@ -724,7 +868,10 @@ key_dialog_save (GtkWidget *wid, gpointer userdata)
|
||||
}
|
||||
|
||||
if (key_save_kbs () == 0)
|
||||
{
|
||||
menu_update_quit_accel ();
|
||||
key_dialog_close (wid, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -737,6 +884,7 @@ key_dialog_add (GtkWidget *wid, gpointer userdata)
|
||||
GtkTreePath *path;
|
||||
|
||||
gtk_list_store_append (store, &iter);
|
||||
gtk_list_store_set (store, &iter, CUSTOM_COLUMN, TRUE, -1);
|
||||
|
||||
/* make sure the new row is visible and selected */
|
||||
path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
|
||||
@@ -746,6 +894,134 @@ key_dialog_add (GtkWidget *wid, gpointer userdata)
|
||||
gtk_tree_path_free (path);
|
||||
}
|
||||
|
||||
static char *
|
||||
key_binding_signature (const char *action, const char *data1, const char *data2)
|
||||
{
|
||||
return g_strdup_printf ("%s\\n%s\\n%s", action ? action : "", data1 ? data1 : "", data2 ? data2 : "");
|
||||
}
|
||||
|
||||
static int
|
||||
key_dialog_reset_count (GHashTable *table, const char *key)
|
||||
{
|
||||
return GPOINTER_TO_INT (g_hash_table_lookup (table, key));
|
||||
}
|
||||
|
||||
static void
|
||||
key_dialog_reset_increment (GHashTable *table, char *key)
|
||||
{
|
||||
g_hash_table_replace (table, key, GINT_TO_POINTER (key_dialog_reset_count (table, key) + 1));
|
||||
}
|
||||
|
||||
static void
|
||||
key_dialog_reset (GtkWidget *wid, gpointer userdata)
|
||||
{
|
||||
GtkListStore *store = GTK_LIST_STORE (get_store ());
|
||||
GtkListStore *custom_store;
|
||||
GtkTreeIter iter, custom_iter;
|
||||
GtkWidget *delete_button;
|
||||
GHashTable *default_counts, *seen_counts;
|
||||
GSList *list = NULL, *old_list, *default_iter;
|
||||
struct key_binding *kb;
|
||||
gboolean custom, keep;
|
||||
char *key, *accel, *action, *data1, *data2, *signature;
|
||||
|
||||
if (key_load_kbs_from_buffer (g_strdup (default_kb_cfg), strlen (default_kb_cfg), &list) != 0)
|
||||
return;
|
||||
|
||||
default_counts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||||
seen_counts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||||
for (default_iter = list; default_iter; default_iter = g_slist_next (default_iter))
|
||||
{
|
||||
kb = default_iter->data;
|
||||
signature = key_binding_signature (key_actions[kb->action].name, kb->data1, kb->data2);
|
||||
key_dialog_reset_increment (default_counts, signature);
|
||||
}
|
||||
|
||||
custom_store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
|
||||
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
|
||||
|
||||
if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter))
|
||||
{
|
||||
do
|
||||
{
|
||||
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter,
|
||||
KEY_COLUMN, &key,
|
||||
ACCEL_COLUMN, &accel,
|
||||
ACTION_COLUMN, &action,
|
||||
D1_COLUMN, &data1,
|
||||
D2_COLUMN, &data2,
|
||||
CUSTOM_COLUMN, &custom,
|
||||
-1);
|
||||
signature = key_binding_signature (action, data1, data2);
|
||||
keep = custom || key_dialog_reset_count (seen_counts, signature) >= key_dialog_reset_count (default_counts, signature);
|
||||
if (!custom)
|
||||
key_dialog_reset_increment (seen_counts, g_strdup (signature));
|
||||
if (keep)
|
||||
{
|
||||
gtk_list_store_append (custom_store, &custom_iter);
|
||||
gtk_list_store_set (custom_store, &custom_iter,
|
||||
KEY_COLUMN, key,
|
||||
ACCEL_COLUMN, accel,
|
||||
ACTION_COLUMN, action,
|
||||
D1_COLUMN, data1,
|
||||
D2_COLUMN, data2,
|
||||
CUSTOM_COLUMN, TRUE,
|
||||
-1);
|
||||
}
|
||||
g_free (signature);
|
||||
g_free (key);
|
||||
g_free (accel);
|
||||
g_free (action);
|
||||
g_free (data1);
|
||||
g_free (data2);
|
||||
}
|
||||
while (gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter));
|
||||
}
|
||||
|
||||
old_list = keybind_list;
|
||||
keybind_list = list;
|
||||
gtk_list_store_clear (store);
|
||||
key_dialog_load (store);
|
||||
keybind_list = old_list;
|
||||
|
||||
if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (custom_store), &iter))
|
||||
{
|
||||
do
|
||||
{
|
||||
gtk_tree_model_get (GTK_TREE_MODEL (custom_store), &iter,
|
||||
KEY_COLUMN, &key,
|
||||
ACCEL_COLUMN, &accel,
|
||||
ACTION_COLUMN, &action,
|
||||
D1_COLUMN, &data1,
|
||||
D2_COLUMN, &data2,
|
||||
-1);
|
||||
gtk_list_store_append (store, &custom_iter);
|
||||
gtk_list_store_set (store, &custom_iter,
|
||||
KEY_COLUMN, key,
|
||||
ACCEL_COLUMN, accel,
|
||||
ACTION_COLUMN, action,
|
||||
D1_COLUMN, data1,
|
||||
D2_COLUMN, data2,
|
||||
CUSTOM_COLUMN, TRUE,
|
||||
-1);
|
||||
g_free (key);
|
||||
g_free (accel);
|
||||
g_free (action);
|
||||
g_free (data1);
|
||||
g_free (data2);
|
||||
}
|
||||
while (gtk_tree_model_iter_next (GTK_TREE_MODEL (custom_store), &iter));
|
||||
}
|
||||
|
||||
delete_button = g_object_get_data (G_OBJECT (key_dialog), "delete_button");
|
||||
if (delete_button)
|
||||
gtk_widget_set_sensitive (delete_button, FALSE);
|
||||
g_hash_table_destroy (default_counts);
|
||||
g_hash_table_destroy (seen_counts);
|
||||
g_object_unref (custom_store);
|
||||
g_slist_free_full (list, key_free);
|
||||
}
|
||||
|
||||
static void
|
||||
key_dialog_delete (GtkWidget *wid, gpointer userdata)
|
||||
{
|
||||
@@ -753,9 +1029,14 @@ key_dialog_delete (GtkWidget *wid, gpointer userdata)
|
||||
GtkListStore *store = GTK_LIST_STORE (gtk_tree_view_get_model (view));
|
||||
GtkTreeIter iter;
|
||||
GtkTreePath *path;
|
||||
gboolean custom;
|
||||
|
||||
if (gtkutil_treeview_get_selected (view, &iter, -1))
|
||||
{
|
||||
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, CUSTOM_COLUMN, &custom, -1);
|
||||
if (!custom)
|
||||
return;
|
||||
|
||||
/* delete this row, select next one */
|
||||
if (gtk_list_store_remove (store, &iter))
|
||||
{
|
||||
@@ -782,13 +1063,13 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
|
||||
|
||||
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
|
||||
G_TYPE_STRING, G_TYPE_STRING);
|
||||
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
|
||||
g_return_val_if_fail (store != NULL, NULL);
|
||||
|
||||
view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
|
||||
gtk_tree_view_set_fixed_height_mode (GTK_TREE_VIEW (view), TRUE);
|
||||
gtk_tree_view_set_enable_search (GTK_TREE_VIEW (view), FALSE);
|
||||
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view), TRUE);
|
||||
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view), FALSE);
|
||||
|
||||
g_signal_connect (G_OBJECT (view), "key-press-event",
|
||||
G_CALLBACK (key_dialog_keypress), NULL);
|
||||
@@ -858,7 +1139,8 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
G_CALLBACK (key_dialog_combo_changed), combostore);
|
||||
gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (view), ACTION_COLUMN,
|
||||
"Action", render,
|
||||
"text", ACTION_COLUMN,
|
||||
"text", ACTION_COLUMN,
|
||||
"editable", CUSTOM_COLUMN,
|
||||
NULL);
|
||||
|
||||
render = gtk_cell_renderer_text_new ();
|
||||
@@ -869,6 +1151,7 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
GTK_TREE_VIEW (view), D1_COLUMN,
|
||||
"Data1", render,
|
||||
"text", D1_COLUMN,
|
||||
"editable", CUSTOM_COLUMN,
|
||||
NULL);
|
||||
|
||||
render = gtk_cell_renderer_text_new ();
|
||||
@@ -879,6 +1162,7 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
GTK_TREE_VIEW (view), D2_COLUMN,
|
||||
"Data2", render,
|
||||
"text", D2_COLUMN,
|
||||
"editable", CUSTOM_COLUMN,
|
||||
NULL);
|
||||
|
||||
col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), KEY_COLUMN);
|
||||
@@ -924,7 +1208,8 @@ key_dialog_load (GtkListStore *store)
|
||||
ACCEL_COLUMN, accel_text,
|
||||
ACTION_COLUMN, key_actions[kb->action].name,
|
||||
D1_COLUMN, kb->data1,
|
||||
D2_COLUMN, kb->data2, -1);
|
||||
D2_COLUMN, kb->data2,
|
||||
CUSTOM_COLUMN, !key_binding_is_builtin (kb), -1);
|
||||
|
||||
g_free (accel_text);
|
||||
g_free (label_text);
|
||||
@@ -937,7 +1222,7 @@ void
|
||||
key_dialog_show ()
|
||||
{
|
||||
GtkWidget *vbox, *box;
|
||||
GtkWidget *view, *xtext;
|
||||
GtkWidget *view, *xtext, *delete_button;
|
||||
GtkListStore *store;
|
||||
XTextColor xtext_palette[XTEXT_COLS];
|
||||
char buf[128];
|
||||
@@ -971,8 +1256,12 @@ key_dialog_show ()
|
||||
|
||||
gtkutil_button (box, ICON_FKEYS_NEW, NULL, key_dialog_add,
|
||||
NULL, _("Add"));
|
||||
gtkutil_button (box, ICON_FKEYS_DELETE, NULL, key_dialog_delete,
|
||||
delete_button = gtkutil_button (box, ICON_FKEYS_DELETE, NULL, key_dialog_delete,
|
||||
NULL, _("Delete"));
|
||||
g_object_set_data (G_OBJECT (key_dialog), "delete_button", delete_button);
|
||||
gtk_widget_set_sensitive (delete_button, FALSE);
|
||||
gtkutil_button (box, ICON_FKEYS_RESET, NULL, key_dialog_reset,
|
||||
NULL, _("Reset"));
|
||||
gtkutil_button (box, ICON_FKEYS_CANCEL, NULL, key_dialog_close,
|
||||
NULL, _("Cancel"));
|
||||
gtkutil_button (box, ICON_FKEYS_SAVE, NULL, key_dialog_save,
|
||||
@@ -1080,41 +1369,14 @@ key_load_kbs_helper_mod (char *buf, GdkModifierType *out)
|
||||
}
|
||||
|
||||
static int
|
||||
key_load_kbs (void)
|
||||
key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list)
|
||||
{
|
||||
char *buf, *ibuf;
|
||||
struct stat st;
|
||||
char *buf;
|
||||
struct key_binding *kb = NULL;
|
||||
int fd, len, state = 0, pnt = 0;
|
||||
int len, state = 0, pnt = 0;
|
||||
guint keyval;
|
||||
GdkModifierType mod = 0;
|
||||
off_t size;
|
||||
|
||||
fd = zoitechat_open_file ("keybindings.conf", O_RDONLY, 0, 0);
|
||||
if (fd < 0)
|
||||
{
|
||||
ibuf = g_strdup (default_kb_cfg);
|
||||
size = strlen (default_kb_cfg);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fstat (fd, &st) != 0)
|
||||
{
|
||||
close (fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ibuf = g_malloc(st.st_size);
|
||||
read (fd, ibuf, st.st_size);
|
||||
size = st.st_size;
|
||||
close (fd);
|
||||
}
|
||||
|
||||
if (keybind_list)
|
||||
{
|
||||
g_slist_free_full (keybind_list, key_free);
|
||||
keybind_list = NULL;
|
||||
}
|
||||
GSList *list = NULL;
|
||||
|
||||
while (buf_get_line (ibuf, &buf, &pnt, size))
|
||||
{
|
||||
@@ -1128,14 +1390,12 @@ key_load_kbs (void)
|
||||
case KBSTATE_MOD:
|
||||
kb = g_new0 (struct key_binding, 1);
|
||||
|
||||
/* New format */
|
||||
if (strncmp (buf, "ACCEL=", 6) == 0)
|
||||
{
|
||||
buf += 6;
|
||||
|
||||
gtk_accelerator_parse (buf, &keyval, &mod);
|
||||
|
||||
|
||||
kb->keyval = keyval;
|
||||
kb->mod = key_modifier_get_valid (mod);
|
||||
|
||||
@@ -1158,6 +1418,8 @@ key_load_kbs (void)
|
||||
if (keyval == 0)
|
||||
{
|
||||
g_free (ibuf);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -1174,6 +1436,8 @@ key_load_kbs (void)
|
||||
if (kb->action == KEY_MAX_ACTIONS + 1)
|
||||
{
|
||||
g_free (ibuf);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -1191,6 +1455,8 @@ key_load_kbs (void)
|
||||
if (buf[0] != 'D')
|
||||
{
|
||||
g_free (ibuf);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 4;
|
||||
}
|
||||
|
||||
@@ -1211,7 +1477,6 @@ key_load_kbs (void)
|
||||
if (buf[2] == ':')
|
||||
{
|
||||
len = strlen (buf);
|
||||
/* Add one for the NULL, subtract 3 for the "Dx:" */
|
||||
len++;
|
||||
len -= 3;
|
||||
if (state == KBSTATE_DT1)
|
||||
@@ -1234,7 +1499,8 @@ key_load_kbs (void)
|
||||
continue;
|
||||
} else
|
||||
{
|
||||
keybind_list = g_slist_append (keybind_list, kb);
|
||||
list = g_slist_append (list, kb);
|
||||
kb = NULL;
|
||||
|
||||
state = KBSTATE_MOD;
|
||||
}
|
||||
@@ -1243,14 +1509,56 @@ key_load_kbs (void)
|
||||
}
|
||||
}
|
||||
g_free (ibuf);
|
||||
*out_list = list;
|
||||
return 0;
|
||||
|
||||
corrupt_file:
|
||||
g_free (ibuf);
|
||||
g_free (kb);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 5;
|
||||
}
|
||||
|
||||
static int
|
||||
key_load_kbs (void)
|
||||
{
|
||||
char *ibuf;
|
||||
struct stat st;
|
||||
int fd, result;
|
||||
off_t size;
|
||||
GSList *list = NULL;
|
||||
|
||||
fd = zoitechat_open_file ("keybindings.conf", O_RDONLY, 0, 0);
|
||||
if (fd < 0)
|
||||
{
|
||||
ibuf = g_strdup (default_kb_cfg);
|
||||
size = strlen (default_kb_cfg);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fstat (fd, &st) != 0)
|
||||
{
|
||||
close (fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ibuf = g_malloc(st.st_size);
|
||||
read (fd, ibuf, st.st_size);
|
||||
size = st.st_size;
|
||||
close (fd);
|
||||
}
|
||||
|
||||
result = key_load_kbs_from_buffer (ibuf, size, &list);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
if (keybind_list)
|
||||
g_slist_free_full (keybind_list, key_free);
|
||||
keybind_list = list;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
key_action_handle_command (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
@@ -1268,6 +1576,24 @@ key_action_handle_command (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
key_action_menu_shortcut (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
{
|
||||
if (menu_key_action (d1, evt->keyval, evt->state))
|
||||
return 2;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
key_action_reopen_closed_tab (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
{
|
||||
mg_reopen_closed_channel_tab ();
|
||||
return 2;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if the given session is inside the main window. This predicate
|
||||
* is passed to lastact_getfirst() as a way to filter out detached sessions.
|
||||
@@ -1286,6 +1612,9 @@ key_action_page_switch (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
{
|
||||
session *newsess;
|
||||
char *network;
|
||||
char *channel;
|
||||
char *slash;
|
||||
int len, i, num;
|
||||
|
||||
if (!d1)
|
||||
@@ -1297,21 +1626,13 @@ key_action_page_switch (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
|
||||
if (g_ascii_strcasecmp(d1, "auto") == 0)
|
||||
{
|
||||
/* Auto switch makes no sense in detached sessions */
|
||||
if (!sess->gui->is_tab)
|
||||
return 1;
|
||||
|
||||
/* Obtain a session with recent activity */
|
||||
newsess = lastact_getfirst(session_check_is_tab);
|
||||
|
||||
if (newsess)
|
||||
{
|
||||
/*
|
||||
* Only sessions in the current window should be considered (i.e.
|
||||
* we don't want to move the focus on a different window). This
|
||||
* call could, in theory, do this, but we checked before that
|
||||
* newsess->gui->is_tab and sess->gui->is_tab.
|
||||
*/
|
||||
mg_bring_tofront_sess(newsess);
|
||||
return 0;
|
||||
}
|
||||
@@ -1325,8 +1646,27 @@ key_action_page_switch (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
{
|
||||
if (i == 0 && (d1[i] == '+' || d1[i] == '-'))
|
||||
continue;
|
||||
else
|
||||
return 1;
|
||||
|
||||
network = NULL;
|
||||
channel = d1;
|
||||
slash = strchr (d1, '/');
|
||||
|
||||
if (slash && slash[1])
|
||||
{
|
||||
network = g_strndup (d1, slash - d1);
|
||||
channel = slash + 1;
|
||||
}
|
||||
|
||||
newsess = plugin_find_context (network, channel, sess->server);
|
||||
g_free (network);
|
||||
|
||||
if (newsess && newsess->gui)
|
||||
{
|
||||
mg_bring_tofront_sess(newsess);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1952,8 +2292,37 @@ replace_handle (GtkWidget *t)
|
||||
{
|
||||
ptrdiff_t found_offset = found - text;
|
||||
ptrdiff_t found_end_offset = found_offset + (ptrdiff_t) pop_len;
|
||||
gboolean start_ok;
|
||||
gboolean end_ok;
|
||||
int rank;
|
||||
ptrdiff_t distance;
|
||||
const char *before = found;
|
||||
const char *after = found + pop_len;
|
||||
|
||||
if (before > text)
|
||||
{
|
||||
before = g_utf8_find_prev_char (text, before);
|
||||
start_ok = !before || (!g_unichar_isalnum (g_utf8_get_char (before)) && g_utf8_get_char (before) != '_');
|
||||
}
|
||||
else
|
||||
{
|
||||
start_ok = TRUE;
|
||||
}
|
||||
|
||||
if (*after != '\0')
|
||||
{
|
||||
end_ok = !g_unichar_isalnum (g_utf8_get_char (after)) && g_utf8_get_char (after) != '_';
|
||||
}
|
||||
else
|
||||
{
|
||||
end_ok = TRUE;
|
||||
}
|
||||
|
||||
if (!start_ok || !end_ok)
|
||||
{
|
||||
found++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cursor_byte_offset >= found_offset && cursor_byte_offset <= found_end_offset)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user