Compare commits
206 Commits
Author | SHA1 | Date | |
---|---|---|---|
07873ca4e9 | |||
bde8d9d20a | |||
c550dc9cb1 | |||
df8f516a40 | |||
67b25fddf1 | |||
bb7a03e9f6 | |||
9d175cc459 | |||
4ad7afe884 | |||
221283ba19 | |||
8cf2aa5586 | |||
8fb0d2311f | |||
2dbc6adbc2 | |||
bd4290a1a9 | |||
46c9df1863 | |||
20c50fd7ef | |||
d7c6c424e8 | |||
ec9653e754 | |||
dfda8f2eee | |||
b8645bfbf2 | |||
778047bc65 | |||
2638c88479 | |||
6da8f97e37 | |||
2dd7636134 | |||
13b6a40b9c | |||
dd167b4c83 | |||
133f628064 | |||
d99a98ff4c | |||
94efa378f7 | |||
ccf6f431bb | |||
73c0b672a2 | |||
7cff05c7ac | |||
1de339dfbc | |||
a330c1cf4d | |||
7df34cdcb2 | |||
91adfb5917 | |||
9c7109b578 | |||
d936b653ac | |||
d889a8e019 | |||
66f5968225 | |||
ba5d79b496 | |||
7c27dcd524 | |||
d07e8a8ab2 | |||
3ebb2c5eec | |||
bbd60a96ec | |||
8443755772 | |||
f93b13a6a3 | |||
4f3ef3505a | |||
b54593e752 | |||
64da6ce1fc | |||
3f099bace2 | |||
dac8ace90c | |||
f42f6af1b9 | |||
9039a5d75b | |||
aabe3438fa | |||
6fd8a8f9bf | |||
40399b1cb6 | |||
dd6f53f504 | |||
3f07670b34 | |||
2985dde7f0 | |||
8239fbd041 | |||
899b4cd3eb | |||
ef0e670392 | |||
69ce388a87 | |||
fee86de499 | |||
91439f04c0 | |||
c144d0468b | |||
482efae89a | |||
cbb0927a7a | |||
25440a07c3 | |||
869a8d7ab3 | |||
c8536ed50c | |||
cfb43bf550 | |||
816769af5b | |||
c5e0b22c55 | |||
c9145a1460 | |||
199c03c8c6 | |||
cdcdeacd63 | |||
28a4726ddc | |||
6b7d110ced | |||
d5b4577315 | |||
55e4f1c42e | |||
08e13a3ac5 | |||
f5926fbd23 | |||
623d93c6f1 | |||
1f608e600b | |||
747a52aae8 | |||
1f5c95d9e9 | |||
09e9d1f749 | |||
333a02d015 | |||
734d888210 | |||
4fc22a978a | |||
7f8b0a19cf | |||
cdfc3b9ea9 | |||
076b2c1c73 | |||
7121bb6e82 | |||
da26097aab | |||
e03fab07ed | |||
0a85d79dff | |||
d3545f37cd | |||
ad20708766 | |||
37118a4d2b | |||
6199635e7f | |||
c64dda4dea | |||
5310f451f2 | |||
65930492ca | |||
04acbdc221 | |||
e2ec2c9ab7 | |||
939ec7a16e | |||
29e78d3851 | |||
c06f6f2565 | |||
e4fd69e3d4 | |||
f0554b27df | |||
65edc9ad9a | |||
a25f238168 | |||
90c91d6c9a | |||
090fd29acf | |||
cc04916137 | |||
964ae72fa8 | |||
87eb728147 | |||
078af20e8b | |||
bd3f3fa5f7 | |||
df818ad7d9 | |||
c7844c775a | |||
4758d3705d | |||
bbbc2aad1b | |||
7a275812c0 | |||
453cb7ca79 | |||
163608d7fd | |||
71eb79fee4 | |||
aec72593f2 | |||
c5a798beec | |||
2f376953f3 | |||
53952feddd | |||
f9adf88eca | |||
82a424fc8a | |||
c2cdf0d2a1 | |||
83daed8706 | |||
5d5838e712 | |||
082f2f8ceb | |||
7b950eb021 | |||
37192a9136 | |||
3871fbaacb | |||
5deb695919 | |||
bcff9a2ad8 | |||
9c44d7baf4 | |||
c361bdca6a | |||
c522ccce7f | |||
58cdff728d | |||
bfd6eea98f | |||
eeada79a64 | |||
202393a77c | |||
d9809f2787 | |||
ea2f298a1a | |||
7d9f3acfc9 | |||
ad5be08a07 | |||
308838da32 | |||
92014628d1 | |||
586f089df6 | |||
a67eafc796 | |||
5382401893 | |||
8bb768ef93 | |||
ed1d5061a4 | |||
468ce821fe | |||
87470f30a9 | |||
ba72cc7b6d | |||
c1091c38b8 | |||
804f959a1d | |||
7abeb10cf1 | |||
a5a727122b | |||
f7713a6a64 | |||
3ebfa83fdd | |||
ed55330153 | |||
a2ff661d40 | |||
706f9bca82 | |||
6432694455 | |||
cf140f3ab0 | |||
c7322f406c | |||
18eae24acf | |||
c092af89a2 | |||
2a8ab8bb7f | |||
8665501c77 | |||
7659caada1 | |||
fd47adf595 | |||
cadc51ede9 | |||
57478b6575 | |||
5c5aacd9da | |||
93cc105a40 | |||
33300630a3 | |||
fd2167d856 | |||
08fb808ea4 | |||
c70c1e1896 | |||
5cd70622aa | |||
5ca767f7f8 | |||
111441302c | |||
ed6f544572 | |||
ee85129a9b | |||
93f926bf12 | |||
da56297c5a | |||
5d8b4719a8 | |||
8a875afad0 | |||
dc483b2342 | |||
28a3d42ad1 | |||
eb942fc274 | |||
27acca0f5b | |||
ececf2f640 | |||
d72249d91f |
17
.github/workflows/flatpak-build.yml
vendored
Normal file
17
.github/workflows/flatpak-build.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: Flatpak Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
flatpak_build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: bilelmoussaoui/flatpak-github-actions:gnome-40
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v3
|
||||
with:
|
||||
bundle: hexchat.flatpak
|
||||
manifest-path: flatpak/io.github.Hexchat.json
|
41
.github/workflows/msys-build.yml
vendored
Normal file
41
.github/workflows/msys-build.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: MSYS2 Build
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
msys2_build:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
install: >-
|
||||
mingw-w64-x86_64-gcc
|
||||
mingw-w64-x86_64-pkg-config
|
||||
mingw-w64-x86_64-python3-cffi
|
||||
mingw-w64-x86_64-meson
|
||||
mingw-w64-x86_64-gtk2
|
||||
mingw-w64-x86_64-gtk-update-icon-cache
|
||||
mingw-w64-x86_64-luajit
|
||||
mingw-w64-x86_64-desktop-file-utils
|
||||
|
||||
- name: Configure
|
||||
run: >-
|
||||
meson build
|
||||
-Dtext-frontend=true
|
||||
-Ddbus=disabled
|
||||
-Dwith-upd=false
|
||||
-Dwith-perl=false
|
||||
|
||||
- name: Build
|
||||
run: ninja -C build
|
||||
|
||||
- name: Test
|
||||
run: ninja -C build test
|
||||
|
||||
- name: Install
|
||||
run: ninja -C build install
|
25
.github/workflows/ubuntu-build.yml
vendored
Normal file
25
.github/workflows/ubuntu-build.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Ubuntu Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
ubuntu_build:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y meson libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libpci-dev libperl-dev libssl-dev python3-dev python3-cffi mono-devel desktop-file-utils
|
||||
|
||||
- name: Configure
|
||||
run: meson build -Dtext=true -Dtheme-manager=true -Dauto_features=enabled
|
||||
|
||||
- name: Build
|
||||
run: ninja -C build
|
||||
|
||||
- name: Test
|
||||
run: ninja -C build test
|
||||
|
||||
- name: Install
|
||||
run: sudo ninja -C build install
|
72
.github/workflows/windows-build.yml
vendored
Normal file
72
.github/workflows/windows-build.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
name: Windows Build
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
windows_build:
|
||||
runs-on: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [x64, win32]
|
||||
arch: [x64, x86]
|
||||
exclude:
|
||||
- platform: x64
|
||||
arch: x86
|
||||
- platform: win32
|
||||
arch: x64
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
New-Item -Name "deps" -ItemType "Directory"
|
||||
|
||||
Invoke-WebRequest http://files.jrsoftware.org/is/5/innosetup-5.5.9-unicode.exe -OutFile deps\innosetup-unicode.exe
|
||||
& deps\innosetup-unicode.exe /VERYSILENT | Out-Null
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/misc/idpsetup-1.5.1.exe -OutFile deps\idpsetup.exe
|
||||
& deps\idpsetup.exe /VERYSILENT
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/gtk/gtk-${{ matrix.platform }}-2018-08-29-openssl1.1.7z -OutFile deps\gtk-${{ matrix.arch }}.7z
|
||||
& 7z.exe x deps\gtk-${{ matrix.arch }}.7z -oC:\gtk-build\gtk
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/gtk-win32/gendef-20111031.7z -OutFile deps\gendef.7z
|
||||
& 7z.exe x deps\gendef.7z -oC:\gtk-build
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/gtk-win32/WinSparkle-20151011.7z -OutFile deps\WinSparkle.7z
|
||||
& 7z.exe x deps\WinSparkle.7z -oC:\gtk-build\WinSparkle
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/misc/perl/perl-5.20.0-${{ matrix.arch }}.7z -OutFile deps\perl-${{ matrix.arch }}.7z
|
||||
& 7z.exe x deps\perl-${{ matrix.arch }}.7z -oC:\gtk-build\perl-5.20\${{ matrix.platform }}
|
||||
|
||||
New-Item -Path "c:\gtk-build" -Name "python-2.7" -ItemType "Directory"
|
||||
New-Item -Path "c:\gtk-build" -Name "python-3.8" -ItemType "Directory"
|
||||
New-Item -Path "c:\gtk-build\python-2.7" -Name "${{ matrix.platform }}" -ItemType "SymbolicLink" -Value "C:/hostedtoolcache/windows/Python/2.7.18/${{ matrix.arch }}"
|
||||
New-Item -Path "c:\gtk-build\python-3.8" -Name "${{ matrix.platform }}" -ItemType "SymbolicLink" -Value "C:/hostedtoolcache/windows/Python/3.8.10/${{ matrix.arch }}"
|
||||
|
||||
C:/hostedtoolcache/windows/Python/3.8.10/${{ matrix.arch }}/python.exe -m pip install cffi
|
||||
C:/hostedtoolcache/windows/Python/2.7.18/${{ matrix.arch }}/python.exe -m pip install -qq cffi
|
||||
shell: powershell
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat"
|
||||
msbuild win32\hexchat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
|
||||
shell: cmd
|
||||
|
||||
- name: Preparing Artifacts
|
||||
run: |
|
||||
move ..\hexchat-build\${{ matrix.platform }}\HexChat*.exe .\
|
||||
move ..\hexchat-build .\
|
||||
shell: cmd
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Installer ${{ matrix.arch }}
|
||||
path: HexChat*.exe
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Build Files ${{ matrix.arch }}
|
||||
path: hexchat-build
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "flatpak/shared-modules"]
|
||||
path = flatpak/shared-modules
|
||||
url = https://github.com/flathub/shared-modules.git
|
20
.travis.yml
20
.travis.yml
@ -1,20 +0,0 @@
|
||||
sudo: required
|
||||
services: docker
|
||||
before_install:
|
||||
- docker pull ubuntu:16.04
|
||||
- docker run --privileged --cidfile=/tmp/cid ubuntu:16.04 /bin/sh -c 'apt-get update && apt-get install -y meson/xenial-backports libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libnotify-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev mono-devel desktop-file-utils'
|
||||
- docker commit `cat /tmp/cid` hexchat/ubuntu-ci
|
||||
- rm -f /tmp/cid
|
||||
install:
|
||||
- docker run -d --privileged --cidfile=/tmp/cid --volume=${PWD}:/opt/hexchat hexchat/ubuntu-ci /bin/systemd --system
|
||||
script:
|
||||
- docker exec `cat /tmp/cid` /bin/sh -c 'meson /opt/hexchat /opt/hexchat-build -Dwith-text=true -Dwith-theme-manager=true && ninja -C /opt/hexchat-build install'
|
||||
after_script:
|
||||
- docker kill `cat /tmp/cid`
|
||||
notifications:
|
||||
irc:
|
||||
channels: "chat.freenode.net#hexchat-devel"
|
||||
template: "Build %{build_url} (%{commit} in %{branch}) by %{author}: %{message}"
|
||||
on_success: change
|
||||
matrix:
|
||||
fast_finish: true
|
@ -1,9 +1,11 @@
|
||||
icondir = join_paths(get_option('datadir'), 'icons/hicolor')
|
||||
install_data(
|
||||
'hexchat.png',
|
||||
rename: 'io.github.Hexchat.png',
|
||||
install_dir: join_paths(icondir, '48x48/apps')
|
||||
)
|
||||
install_data(
|
||||
'hexchat.svg',
|
||||
rename: 'io.github.Hexchat.svg',
|
||||
install_dir: join_paths(icondir, 'scalable/apps')
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
if get_option('with-plugin')
|
||||
if get_option('plugin')
|
||||
subdir('pkgconfig')
|
||||
endif
|
||||
|
||||
if get_option('with-gtk')
|
||||
if get_option('gtk-frontend')
|
||||
subdir('icons')
|
||||
subdir('misc')
|
||||
subdir('man')
|
||||
elif get_option('with-theme-manager')
|
||||
elif get_option('theme-manager')
|
||||
subdir('misc')
|
||||
endif
|
||||
|
11
data/misc/io.github.Hexchat.Plugin.metainfo.xml.in
Normal file
11
data/misc/io.github.Hexchat.Plugin.metainfo.xml.in
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="addon">
|
||||
<id>io.github.Hexchat.Plugin.@NAME@</id>
|
||||
<extends>io.github.Hexchat</extends>
|
||||
<name>@NAME@ Plugin</name>
|
||||
<summary>@SUMMARY@</summary>
|
||||
<url type="homepage">https://hexchat.github.io/</url>
|
||||
<project_license>@LICENSE@</project_license>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<update_contact>tingping_AT_fedoraproject.org</update_contact>
|
||||
</component>
|
@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id>io.github.Hexchat.desktop</id>
|
||||
<component type="desktop-application">
|
||||
<id>io.github.Hexchat</id>
|
||||
<name>HexChat</name>
|
||||
<launchable type="desktop-id">io.github.Hexchat.desktop</launchable>
|
||||
<developer_name>HexChat</developer_name>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-2.0+</project_license>
|
||||
@ -13,7 +14,6 @@
|
||||
</description>
|
||||
<url type="homepage">http://hexchat.github.io</url>
|
||||
<url type="bugtracker">https://github.com/hexchat/hexchat</url>
|
||||
<url type="translate">https://www.transifex.com/hexchat/hexchat</url>
|
||||
<url type="donation">https://goo.gl/jESZvU</url>
|
||||
<url type="help">https://hexchat.readthedocs.io/en/latest/</url>
|
||||
<screenshots>
|
||||
@ -27,6 +27,67 @@
|
||||
<id>hexchat.desktop</id>
|
||||
</provides>
|
||||
<releases>
|
||||
<release date="2022-02-12" version="2.16.1">
|
||||
<description>
|
||||
<p>This is a minor release with mostly bug-fixes:</p>
|
||||
<ul>
|
||||
<li>Add `-NOOVERRIDE` flag to the `GUI COLOR` command</li>
|
||||
<li>Add `-q` (quiet) flag to the `EXECWRITE` command</li>
|
||||
<li>Rename installed icon to match app-id (Fixes notification icon)</li>
|
||||
<li>Fix escaping already escaped URLs when opening them</li>
|
||||
<li>Fix Python scripts not being opened as UTF-8</li>
|
||||
<li>Fix `TIMER` command supporting decimals regardless of locale</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2021-10-01" version="2.16.0">
|
||||
<description>
|
||||
<p>This is a feature release:</p>
|
||||
<ul>
|
||||
<li>Add support for IRCv3 SETNAME, invite-notify, account-tag, standard replies, and UTF8ONLY</li>
|
||||
<li>Add support for strikethrough formatting</li>
|
||||
<li>Fix text clipping issues by respecting font line height</li>
|
||||
<li>Fix URLs not being escaped when opened</li>
|
||||
<li>Fix possible hang when showing notifications</li>
|
||||
<li>Print ChanServ notices in the front tab by default</li>
|
||||
<li>Update network list</li>
|
||||
<li>python: Rewrite plugin improving memory usage and compatibility</li>
|
||||
<li>fishlim: Add support for CBC and other improvements</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2019-12-20" version="2.14.3">
|
||||
<description>
|
||||
<p>This is a bug-fix release:</p>
|
||||
<ul>
|
||||
<li>Fix various incorrect parsing of IRC messages relating to trailing parameters</li>
|
||||
<li>Fix SASL negotiation combined with multi-line cap</li>
|
||||
<li>Fix input box theming with Yaru theme</li>
|
||||
<li>python: Work around Python 3.7 regression causing crash on unload</li>
|
||||
<li>sysinfo: Add support for /etc/os-release</li>
|
||||
<li>sysinfo: Ignore irrelevant mounts when calculating storage size</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-08-29" version="2.14.2">
|
||||
<description>
|
||||
<p>This is a minor release:</p>
|
||||
<ul>
|
||||
<li>Remove shift+click to close tab binding</li>
|
||||
<li>Always unminimize when opening from tray</li>
|
||||
<li>Fix some translations containing invalid text events</li>
|
||||
<li>Fix sending server passwords starting with ":"</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-03-13" version="2.14.1">
|
||||
<description>
|
||||
<p>This is a very minor bug-fix release:</p>
|
||||
<ul>
|
||||
<li>Fix performance regression</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-03-10" version="2.14.0">
|
||||
<description>
|
||||
<p>This is largely a bug fix release though it has some large behind the scenes changes:</p>
|
||||
@ -83,5 +144,8 @@
|
||||
<kudo>HiDpiIcon</kudo>
|
||||
<kudo>Notifications</kudo>
|
||||
</kudos>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<update_contact>tingping_at_fedoraproject.org</update_contact>
|
||||
</component>
|
||||
|
@ -4,7 +4,7 @@ GenericName=IRC Client
|
||||
Comment=Chat with other people online
|
||||
Keywords=IM;Chat;
|
||||
Exec=@exec_command@
|
||||
Icon=hexchat
|
||||
Icon=io.github.Hexchat
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=GTK;Network;IRCClient;
|
||||
|
@ -1,24 +1,27 @@
|
||||
appdir = join_paths(get_option('datadir'), 'applications')
|
||||
metainfodir = join_paths(get_option('datadir'), 'metainfo')
|
||||
desktop_utils = find_program('desktop-file-validate', required: false)
|
||||
|
||||
if get_option('with-gtk')
|
||||
hexchat_appdata = i18n.merge_file(
|
||||
input: 'io.github.Hexchat.appdata.xml.in',
|
||||
output: 'io.github.Hexchat.appdata.xml',
|
||||
po_dir: '../../po',
|
||||
install: true,
|
||||
install_dir: join_paths(get_option('datadir'), 'metainfo')
|
||||
)
|
||||
|
||||
appstream_util = find_program('appstream-util', required: false)
|
||||
if appstream_util.found()
|
||||
test('Validate io.github.Hexchat.appdata.xml', appstream_util,
|
||||
args: ['validate-relax', hexchat_appdata]
|
||||
if get_option('gtk-frontend')
|
||||
if get_option('install-appdata')
|
||||
hexchat_appdata = i18n.merge_file(
|
||||
input: 'io.github.Hexchat.appdata.xml.in',
|
||||
output: 'io.github.Hexchat.appdata.xml',
|
||||
po_dir: '../../po',
|
||||
install: true,
|
||||
install_dir: metainfodir
|
||||
)
|
||||
|
||||
appstream_util = find_program('appstream-util', required: false)
|
||||
if appstream_util.found()
|
||||
test('Validate io.github.Hexchat.appdata.xml', appstream_util,
|
||||
args: ['validate-relax', hexchat_appdata]
|
||||
)
|
||||
endif
|
||||
endif
|
||||
|
||||
desktop_conf = configuration_data()
|
||||
if get_option('with-dbus')
|
||||
if dbus_glib_dep.found()
|
||||
desktop_conf.set('exec_command', 'hexchat --existing %U')
|
||||
else
|
||||
desktop_conf.set('exec_command', 'hexchat %U')
|
||||
@ -46,7 +49,7 @@ if get_option('with-gtk')
|
||||
endif
|
||||
endif
|
||||
|
||||
if get_option('with-theme-manager')
|
||||
if get_option('theme-manager')
|
||||
htm_desktop = i18n.merge_file(
|
||||
input: 'io.github.Hexchat.ThemeManager.desktop.in',
|
||||
output: 'io.github.Hexchat.ThemeManager.desktop',
|
||||
@ -66,3 +69,59 @@ if get_option('with-theme-manager')
|
||||
install_dir: join_paths(get_option('datadir'), 'mime/packages')
|
||||
)
|
||||
endif
|
||||
|
||||
if get_option('plugin')
|
||||
plugin_metainfo = []
|
||||
|
||||
# FIXME: These should all get translated somewhere
|
||||
if get_option('with-checksum')
|
||||
plugin_metainfo += [
|
||||
['Checksum', 'Calculates a checksum for all sent and recieved DCC files', 'MIT']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-fishlim')
|
||||
plugin_metainfo += [
|
||||
['Fishlim', 'Allows setting a key for encrypted conversations', 'MIT AND GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-lua') != 'false'
|
||||
plugin_metainfo += [
|
||||
['Lua', 'Provides a scripting interface in Lua', 'MIT']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-perl') != 'false'
|
||||
plugin_metainfo += [
|
||||
['Perl', 'Provides a scripting interface in Perl', 'GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-python') != 'false'
|
||||
plugin_metainfo += [
|
||||
['Python', 'Provides a scripting interface in Python', 'GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-sysinfo')
|
||||
plugin_metainfo += [
|
||||
['Sysinfo', 'Adds command to display system information', 'GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
foreach metainfo : plugin_metainfo
|
||||
name = metainfo[0]
|
||||
conf = configuration_data()
|
||||
conf.set('NAME', name)
|
||||
conf.set('SUMMARY', metainfo[1])
|
||||
conf.set('LICENSE', metainfo[2])
|
||||
|
||||
configure_file(
|
||||
input: 'io.github.Hexchat.Plugin.metainfo.xml.in',
|
||||
output: 'io.github.Hexchat.Plugin.@0@.metainfo.xml'.format(name),
|
||||
configuration: conf,
|
||||
install_dir: get_option('install-plugin-metainfo') ? metainfodir : '',
|
||||
)
|
||||
endforeach
|
||||
endif
|
||||
|
@ -2,8 +2,8 @@ pkg_conf = configuration_data()
|
||||
prefix = get_option('prefix')
|
||||
pkg_conf.set('prefix', prefix)
|
||||
pkg_conf.set('VERSION', meson.project_version())
|
||||
pkg_conf.set('hexchatlibdir', join_paths(prefix, plugindir))
|
||||
pkg_conf.set('includedir', join_paths(prefix, get_option('includedir')))
|
||||
pkg_conf.set('hexchatlibdir', join_paths('${prefix}', plugindir))
|
||||
pkg_conf.set('includedir', join_paths('${prefix}', get_option('includedir')))
|
||||
|
||||
configure_file(
|
||||
input: 'hexchat-plugin.pc.in',
|
||||
|
25
flatpak/Load-plugins-from-Flatpak-extensions.patch
Normal file
25
flatpak/Load-plugins-from-Flatpak-extensions.patch
Normal file
@ -0,0 +1,25 @@
|
||||
From 918503d57c6740d20be68a6717158673a2a8b25f Mon Sep 17 00:00:00 2001
|
||||
From: Patrick Griffis <tingping@tingping.se>
|
||||
Date: Sat, 17 Mar 2018 05:57:49 -0400
|
||||
Subject: [PATCH] Support loading Flatpak extensions
|
||||
|
||||
---
|
||||
src/common/plugin.c | 2 ++
|
||||
1 file changed, 2 insertions(+)
|
||||
|
||||
diff --git a/src/common/plugin.c b/src/common/plugin.c
|
||||
index 3ad3c558..6addf962 100644
|
||||
--- a/src/common/plugin.c
|
||||
+++ b/src/common/plugin.c
|
||||
@@ -450,6 +450,8 @@ plugin_auto_load (session *sess)
|
||||
lib_dir = plugin_get_libdir ();
|
||||
sub_dir = g_build_filename (get_xdir (), "addons", NULL);
|
||||
|
||||
+ for_files ("/app/extensions/lib/hexchat/plugins", "*.so", plugin_auto_load_cb);
|
||||
+
|
||||
#ifdef WIN32
|
||||
/* a long list of bundled plugins that should be loaded automatically,
|
||||
* user plugins should go to <config>, leave Program Files alone! */
|
||||
--
|
||||
2.14.3
|
||||
|
78
flatpak/io.github.Hexchat.json
Normal file
78
flatpak/io.github.Hexchat.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"app-id": "io.github.Hexchat",
|
||||
"branch": "stable",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "40",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"command": "hexchat",
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
"--share=network",
|
||||
"--socket=pulseaudio",
|
||||
"--filesystem=xdg-download",
|
||||
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
|
||||
"--talk-name=org.mpris.MediaPlayer2.*"
|
||||
],
|
||||
"add-extensions": {
|
||||
"io.github.Hexchat.Plugin": {
|
||||
"version": "20.08",
|
||||
"directory": "extensions",
|
||||
"add-ld-path": "lib",
|
||||
"merge-dirs": "lib/hexchat/plugins",
|
||||
"subdirectories": true,
|
||||
"no-autodownload": true,
|
||||
"autodelete": true
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
"shared-modules/gtk2/gtk2.json",
|
||||
"shared-modules/gtk2/gtk2-common-themes.json",
|
||||
"shared-modules/dbus-glib/dbus-glib.json",
|
||||
"shared-modules/lua5.3/lua-5.3.5.json",
|
||||
"shared-modules/libcanberra/libcanberra.json",
|
||||
"python3-cffi.json",
|
||||
{
|
||||
"name": "lgi",
|
||||
"buildsystem": "meson",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/pavouk/lgi.git",
|
||||
"commit": "95418635aa8151a516d43166227ea2b9d4c4403f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hexchat",
|
||||
"buildsystem": "meson",
|
||||
"config-opts": [
|
||||
"--buildtype=release",
|
||||
"-Ddbus-service-use-appid=true",
|
||||
"-Dwith-perl=false",
|
||||
"-Dwith-lua=lua"
|
||||
],
|
||||
"build-options": {
|
||||
"cflags": "-Wno-error=missing-include-dirs"
|
||||
},
|
||||
"cleanup": [
|
||||
"/share/man"
|
||||
],
|
||||
"post-install": [
|
||||
"install -d /app/extensions"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "dir",
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "Load-plugins-from-Flatpak-extensions.patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
19
flatpak/python3-cffi.json
Normal file
19
flatpak/python3-cffi.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "python3-cffi",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"cffi\" --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/0f/86/e19659527668d70be91d0369aeaa055b4eb396b0f387a4f92293a20035bd/pycparser-2.20.tar.gz",
|
||||
"sha256": "2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/a8/20/025f59f929bbcaa579704f443a438135918484fffaacfaddba776b374563/cffi-1.14.5.tar.gz",
|
||||
"sha256": "fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
|
||||
}
|
||||
]
|
||||
}
|
1
flatpak/shared-modules
Submodule
1
flatpak/shared-modules
Submodule
Submodule flatpak/shared-modules added at 2c2f8fef2e
72
meson.build
72
meson.build
@ -1,6 +1,6 @@
|
||||
project('hexchat', 'c',
|
||||
version: '2.14.0',
|
||||
meson_version: '>= 0.38.0',
|
||||
version: '2.16.1',
|
||||
meson_version: '>= 0.47.0',
|
||||
default_options: [
|
||||
'c_std=gnu89',
|
||||
'buildtype=debugoptimized',
|
||||
@ -15,12 +15,17 @@ cc = meson.get_compiler('c')
|
||||
|
||||
libgio_dep = dependency('gio-2.0', version: '>= 2.34.0')
|
||||
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'))
|
||||
|
||||
global_deps = []
|
||||
if cc.get_id() == 'msvc'
|
||||
libssl_dep = cc.find_library('libeay32')
|
||||
libssl_dep = cc.find_library('libssl')
|
||||
else
|
||||
libssl_dep = dependency('openssl', version: '>= 0.9.8',
|
||||
required: get_option('with-ssl'))
|
||||
required: get_option('tls'))
|
||||
endif
|
||||
|
||||
config_h = configuration_data()
|
||||
@ -29,14 +34,14 @@ config_h.set_quoted('PACKAGE_NAME', meson.project_name())
|
||||
config_h.set_quoted('GETTEXT_PACKAGE', 'hexchat')
|
||||
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
|
||||
get_option('datadir'), 'locale'))
|
||||
config_h.set_quoted('G_LOG_DOMAIN', 'hexchat')
|
||||
config_h.set10('ENABLE_NLS', true)
|
||||
|
||||
# Optional features
|
||||
config_h.set('USE_OPENSSL', get_option('with-ssl'))
|
||||
config_h.set('USE_LIBPROXY', get_option('with-libproxy'))
|
||||
config_h.set('USE_LIBCANBERRA', get_option('with-libcanberra'))
|
||||
config_h.set('USE_DBUS', get_option('with-dbus'))
|
||||
config_h.set('USE_PLUGIN', get_option('with-plugin'))
|
||||
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_PLUGIN', get_option('plugin'))
|
||||
|
||||
config_h.set('G_DISABLE_SINGLE_INCLUDES', true)
|
||||
config_h.set('GTK_DISABLE_DEPRECATED', true)
|
||||
@ -49,6 +54,10 @@ config_h.set('GLIB_VERSION_MIN_REQUIRED', 'GLIB_VERSION_2_34')
|
||||
config_h.set('HAVE_MEMRCHR', cc.has_function('memrchr'))
|
||||
config_h.set('HAVE_STRINGS_H', cc.has_header('strings.h'))
|
||||
|
||||
config_h.set_quoted('HEXCHATLIBDIR',
|
||||
join_paths(get_option('prefix'), get_option('libdir'), 'hexchat/plugins')
|
||||
)
|
||||
|
||||
if libssl_dep.found()
|
||||
config_h.set('HAVE_X509_GET_SIGNATURE_NID',
|
||||
cc.has_function('X509_get_signature_nid', dependencies: libssl_dep)
|
||||
@ -87,8 +96,6 @@ endif
|
||||
|
||||
global_cflags = []
|
||||
test_cflags = [
|
||||
'-pipe',
|
||||
'-fPIE',
|
||||
'-funsigned-char',
|
||||
'-Wno-conversion',
|
||||
'-Wno-pointer-sign',
|
||||
@ -131,20 +138,27 @@ global_ldflags = []
|
||||
test_ldflags = [
|
||||
'-Wl,-z,relro',
|
||||
'-Wl,-z,now',
|
||||
'-Wl,-pie',
|
||||
# mingw
|
||||
'-Wl,--dynamicbase',
|
||||
'-Wl,--nxcompat',
|
||||
]
|
||||
if not (host_machine.system() == 'windows' and get_option('debug'))
|
||||
test_ldflags += '-Wl,--dynamicbase'
|
||||
endif
|
||||
foreach ldflag : test_ldflags
|
||||
if cc.has_argument(ldflag) and cc.links('int main (void) { return 0; }', args: ldflag)
|
||||
if meson.version().version_compare('>= 0.46.0')
|
||||
has_arg = cc.has_link_argument(ldflag)
|
||||
else
|
||||
has_arg = cc.has_argument(ldflag)
|
||||
endif
|
||||
|
||||
if has_arg and cc.links('int main (void) { return 0; }', args: ldflag)
|
||||
global_ldflags += ldflag
|
||||
endif
|
||||
endforeach
|
||||
add_project_link_arguments(global_ldflags, language: 'c')
|
||||
|
||||
subdir('src')
|
||||
if get_option('with-plugin')
|
||||
if get_option('plugin')
|
||||
subdir('plugins')
|
||||
endif
|
||||
if cc.get_id() != 'msvc'
|
||||
@ -152,6 +166,32 @@ if cc.get_id() != 'msvc'
|
||||
subdir('po') # FIXME: build xgettext
|
||||
|
||||
meson.add_install_script('meson_post_install.py',
|
||||
'@0@'.format(get_option('with-theme-manager'))
|
||||
'@0@'.format(get_option('theme-manager'))
|
||||
)
|
||||
endif
|
||||
|
||||
if meson.version().version_compare('>= 0.53.0')
|
||||
summary({
|
||||
'prefix': get_option('prefix'),
|
||||
'bindir': get_option('bindir'),
|
||||
'libdir': get_option('libdir'),
|
||||
'datadir': get_option('datadir'),
|
||||
}, section: 'Directories')
|
||||
|
||||
summary({
|
||||
'TLS (openssl)': libssl_dep.found(),
|
||||
'Plugin Support': get_option('plugin'),
|
||||
'DBus Support': dbus_glib_dep.found(),
|
||||
'libcanberra': libcanberra_dep.found(),
|
||||
}, section: 'Features')
|
||||
|
||||
summary({
|
||||
'Lua': get_option('with-lua'),
|
||||
'Python': get_option('with-python'),
|
||||
'Perl': get_option('with-perl'),
|
||||
'Perl Legacy API': get_option('with-perl-legacy-api'),
|
||||
'FiSH': get_option('with-fishlim'),
|
||||
'Sysinfo': get_option('with-sysinfo'),
|
||||
'DCC Checksum': get_option('with-checksum'),
|
||||
}, section: 'Plugins')
|
||||
endif
|
||||
|
@ -1,33 +1,38 @@
|
||||
option('with-gtk', type: 'boolean',
|
||||
# Applications
|
||||
option('gtk-frontend', type: 'boolean',
|
||||
description: 'Main graphical interface'
|
||||
)
|
||||
option('with-text', type: 'boolean', value: false,
|
||||
option('text-frontend', type: 'boolean', value: false,
|
||||
description: 'Text interface (not generally useful)'
|
||||
)
|
||||
option('with-ssl', type: 'boolean',
|
||||
description: 'Support for TLS connections, requires openssl'
|
||||
)
|
||||
option('with-plugin', type: 'boolean',
|
||||
description: 'Support for loadable plugins'
|
||||
)
|
||||
option('with-dbus', type: 'boolean',
|
||||
description: 'Support used for single-instance and scripting interface, Unix only'
|
||||
)
|
||||
option('with-libproxy', type: 'boolean',
|
||||
description: 'Support for getting proxy information, Unix only'
|
||||
)
|
||||
option('with-libnotify', type: 'boolean',
|
||||
description: 'Support for freedesktop notifications, Unix only'
|
||||
)
|
||||
option('with-libcanberra', type: 'boolean',
|
||||
description: 'Support for sound alerts, Unix only'
|
||||
)
|
||||
option('with-theme-manager', type: 'boolean', value: false,
|
||||
option('theme-manager', type: 'boolean', value: false,
|
||||
description: 'Utility to help manage themes, requires mono/.net'
|
||||
)
|
||||
|
||||
# Features
|
||||
option('tls', type: 'feature', value: 'enabled',
|
||||
description: 'Support for TLS connections, requires openssl'
|
||||
)
|
||||
option('plugin', type: 'boolean',
|
||||
description: 'Support for loadable plugins'
|
||||
)
|
||||
option('dbus', type: 'feature', value: 'auto',
|
||||
description: 'Support used for single-instance and scripting interface, Unix only'
|
||||
)
|
||||
option('libcanberra', type: 'feature', value: 'auto',
|
||||
description: 'Support for sound alerts, Unix only'
|
||||
)
|
||||
|
||||
# Install options
|
||||
option('dbus-service-use-appid', type: 'boolean', value: false,
|
||||
description: 'Rename dbus service to match app-id, required for Flatpak'
|
||||
)
|
||||
option('install-appdata', type: 'boolean',
|
||||
description: 'Install appdata files for app stores'
|
||||
)
|
||||
option('install-plugin-metainfo', type: 'boolean', value: false,
|
||||
description: 'Installs metainfo files for enabled plugins, useful when distros create split packages'
|
||||
)
|
||||
|
||||
# Plugins
|
||||
option('with-checksum', type: 'boolean',
|
||||
@ -42,8 +47,8 @@ option('with-fishlim', type: 'boolean',
|
||||
option('with-lua', type: 'string', value: 'luajit',
|
||||
description: 'Lua scripting plugin, value is pkg-config name to use or "false"'
|
||||
)
|
||||
option('with-perl', type: 'boolean',
|
||||
description: 'Perl scripting plugin'
|
||||
option('with-perl', type: 'string', value: 'perl',
|
||||
description: 'Perl scripting plugin, value is path to perl executable or "false"'
|
||||
)
|
||||
option('with-python', type: 'string', value: 'python3',
|
||||
description: 'Python scripting plugin. value is pkg-config name to use or "false"'
|
||||
@ -57,3 +62,6 @@ option('with-upd', type: 'boolean',
|
||||
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'
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -3,4 +3,5 @@ shared_module('checksum', 'checksum.c',
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'checksum.def',
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -1,5 +1,6 @@
|
||||
shared_module('exec', 'exec.c',
|
||||
dependencies: hexchat_plugin_dep,
|
||||
install: true,
|
||||
install_dir: plugindir
|
||||
install_dir: plugindir,
|
||||
vs_module_defs: 'exec.def',
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -27,15 +28,22 @@
|
||||
#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/blowfish.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#include "keystore.h"
|
||||
#include "fish.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define IB 64
|
||||
static const char fish_base64[64] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
/* rfc 2812; 512 - CR-LF = 510 */
|
||||
static const int MAX_COMMAND_LENGTH = 510;
|
||||
static const char fish_base64[] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
static const signed char fish_unbase64[256] = {
|
||||
IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB,
|
||||
IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB,
|
||||
@ -53,6 +61,12 @@ static const signed char fish_unbase64[256] = {
|
||||
27,28,29,30,31,32,33,34, 35,36,37,IB,IB,IB,IB,IB,
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert Int to 4 Bytes (Big-endian)
|
||||
*
|
||||
* @param int source
|
||||
* @param char* dest
|
||||
*/
|
||||
#define GET_BYTES(dest, source) do { \
|
||||
*((dest)++) = ((source) >> 24) & 0xFF; \
|
||||
*((dest)++) = ((source) >> 16) & 0xFF; \
|
||||
@ -60,135 +74,488 @@ static const signed char fish_unbase64[256] = {
|
||||
*((dest)++) = (source) & 0xFF; \
|
||||
} while (0);
|
||||
|
||||
/**
|
||||
* Convert 4 Bytes to Int (Big-endian)
|
||||
*
|
||||
* @param char* source
|
||||
* @param int dest
|
||||
*/
|
||||
#define GET_LONG(dest, source) do { \
|
||||
dest = ((uint8_t)*((source)++) << 24); \
|
||||
dest |= ((uint8_t)*((source)++) << 16); \
|
||||
dest |= ((uint8_t)*((source)++) << 8); \
|
||||
dest |= (uint8_t)*((source)++); \
|
||||
} while (0);
|
||||
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message) {
|
||||
BF_KEY bfkey;
|
||||
size_t messagelen;
|
||||
size_t i;
|
||||
int j;
|
||||
char *encrypted;
|
||||
char *end;
|
||||
unsigned char bit;
|
||||
unsigned char word;
|
||||
unsigned char d;
|
||||
BF_set_key(&bfkey, keylen, (const unsigned char*)key);
|
||||
|
||||
messagelen = strlen(message);
|
||||
if (messagelen == 0) return NULL;
|
||||
encrypted = g_malloc(((messagelen - 1) / 8) * 12 + 12 + 1); /* each 8-byte block becomes 12 bytes */
|
||||
end = encrypted;
|
||||
|
||||
while (*message) {
|
||||
/* Read 8 bytes (a Blowfish block) */
|
||||
BF_LONG binary[2] = { 0, 0 };
|
||||
unsigned char c;
|
||||
for (i = 0; i < 8; i++) {
|
||||
c = message[i];
|
||||
binary[i >> 2] |= c << 8*(3 - (i&3));
|
||||
if (c == '\0') break;
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
#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;
|
||||
}
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
|
||||
void fish_deinit(void)
|
||||
{
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
if (legacy_provider) {
|
||||
OSSL_PROVIDER_unload(legacy_provider);
|
||||
legacy_provider = NULL;
|
||||
}
|
||||
|
||||
if (default_provider) {
|
||||
OSSL_PROVIDER_unload(default_provider);
|
||||
default_provider = NULL;
|
||||
}
|
||||
|
||||
if (ossl_ctx) {
|
||||
OSSL_LIB_CTX_free(ossl_ctx);
|
||||
ossl_ctx = NULL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode ECB FiSH Base64
|
||||
*
|
||||
* @param [in] message Bytes to encode
|
||||
* @param [in] message_len Size of bytes to encode
|
||||
* @return Array of char with encoded string
|
||||
*/
|
||||
char *fish_base64_encode(const char *message, size_t message_len) {
|
||||
BF_LONG left = 0, right = 0;
|
||||
int i, j;
|
||||
char *encoded = NULL;
|
||||
char *end = NULL;
|
||||
char *msg = NULL;
|
||||
|
||||
if (message_len == 0)
|
||||
return NULL;
|
||||
|
||||
/* Each 8-byte block becomes 12 bytes (fish base64 format) and add 1 byte for \0 */
|
||||
encoded = g_malloc(((message_len - 1) / 8) * 12 + 12 + 1);
|
||||
end = encoded;
|
||||
|
||||
/* Iterate over each 8-byte block (Blowfish block size) */
|
||||
for (j = 0; j < message_len; j += 8) {
|
||||
msg = (char *) message;
|
||||
|
||||
/* Set left and right longs */
|
||||
GET_LONG(left, msg);
|
||||
GET_LONG(right, msg);
|
||||
|
||||
for (i = 0; i < 6; ++i) {
|
||||
*end++ = fish_base64[right & 0x3fu];
|
||||
right = (right >> 6u);
|
||||
}
|
||||
|
||||
for (i = 0; i < 6; ++i) {
|
||||
*end++ = fish_base64[left & 0x3fu];
|
||||
left = (left >> 6u);
|
||||
}
|
||||
|
||||
/* The previous for'd ensure fill all bytes of encoded, we don't need will with zeros */
|
||||
message += 8;
|
||||
|
||||
/* Encrypt block */
|
||||
BF_encrypt(binary, &bfkey);
|
||||
|
||||
/* Emit FiSH-BASE64 */
|
||||
bit = 0;
|
||||
word = 1;
|
||||
for (j = 0; j < 12; j++) {
|
||||
d = fish_base64[(binary[word] >> bit) & 63];
|
||||
*(end++) = d;
|
||||
bit += 6;
|
||||
if (j == 5) {
|
||||
bit = 0;
|
||||
word = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stop if a null terminator was found */
|
||||
if (c == '\0') break;
|
||||
}
|
||||
*end = '\0';
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data) {
|
||||
BF_KEY bfkey;
|
||||
size_t i;
|
||||
char *decrypted;
|
||||
char *end;
|
||||
unsigned char bit;
|
||||
unsigned char word;
|
||||
unsigned char d;
|
||||
BF_set_key(&bfkey, keylen, (const unsigned char*)key);
|
||||
|
||||
decrypted = g_malloc(strlen(data) + 1);
|
||||
end = decrypted;
|
||||
|
||||
while (*data) {
|
||||
/* Convert from FiSH-BASE64 */
|
||||
BF_LONG binary[2] = { 0, 0 };
|
||||
bit = 0;
|
||||
word = 1;
|
||||
for (i = 0; i < 12; i++) {
|
||||
d = fish_unbase64[(const unsigned char)*(data++)];
|
||||
if (d == IB) goto decrypt_end;
|
||||
binary[word] |= (unsigned long)d << bit;
|
||||
bit += 6;
|
||||
if (i == 5) {
|
||||
bit = 0;
|
||||
word = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decrypt block */
|
||||
BF_decrypt(binary, &bfkey);
|
||||
|
||||
/* Copy to buffer */
|
||||
GET_BYTES(end, binary[0]);
|
||||
GET_BYTES(end, binary[1]);
|
||||
}
|
||||
|
||||
decrypt_end:
|
||||
*end = '\0';
|
||||
return decrypted;
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a message (see fish_decrypt). The key is searched for in the
|
||||
* key store.
|
||||
* Decode ECB FiSH Base64
|
||||
*
|
||||
* @param [in] message Base64 encoded string
|
||||
* @param [out] final_len Real length of message
|
||||
* @return Array of char with decoded message
|
||||
*/
|
||||
char *fish_encrypt_for_nick(const char *nick, const char *data) {
|
||||
char *key;
|
||||
char *encrypted;
|
||||
char *fish_base64_decode(const char *message, size_t *final_len) {
|
||||
BF_LONG left, right;
|
||||
int i;
|
||||
char *bytes = NULL;
|
||||
char *msg = NULL;
|
||||
char *byt = NULL;
|
||||
size_t message_len;
|
||||
|
||||
/* Look for key */
|
||||
key = keystore_get_key(nick);
|
||||
if (!key) return NULL;
|
||||
|
||||
/* Encrypt */
|
||||
encrypted = fish_encrypt(key, strlen(key), data);
|
||||
|
||||
g_free(key);
|
||||
return encrypted;
|
||||
message_len = strlen(message);
|
||||
|
||||
/* Ensure blocks of 12 bytes each one and valid characters */
|
||||
if (message_len == 0 || message_len % 12 != 0 || strspn(message, fish_base64) != message_len)
|
||||
return NULL;
|
||||
|
||||
/* Each 12 bytes becomes 8-byte block and add 1 byte for \0 */
|
||||
*final_len = ((message_len - 1) / 12) * 8 + 8 + 1;
|
||||
(*final_len)--; /* We support binary data */
|
||||
bytes = (char *) g_malloc0(*final_len);
|
||||
byt = bytes;
|
||||
|
||||
msg = (char *) message;
|
||||
|
||||
while (*msg) {
|
||||
right = 0;
|
||||
left = 0;
|
||||
for (i = 0; i < 6; i++) right |= (uint8_t) fish_unbase64[(int)*msg++] << (i * 6u);
|
||||
for (i = 0; i < 6; i++) left |= (uint8_t) fish_unbase64[(int)*msg++] << (i * 6u);
|
||||
GET_BYTES(byt, left);
|
||||
GET_BYTES(byt, right);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a message (see fish_decrypt). The key is searched for in the
|
||||
* key store.
|
||||
* Encrypt or decrypt data with Blowfish cipher, support binary data.
|
||||
*
|
||||
* Good documentation for EVP:
|
||||
*
|
||||
* - https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption
|
||||
*
|
||||
* - https://stackoverflow.com/questions/5727646/what-is-the-length-parameter-of-aes-evp-decrypt
|
||||
*
|
||||
* - https://stackoverflow.com/questions/26345175/correct-way-to-free-allocate-the-context-in-the-openssl
|
||||
*
|
||||
* - https://stackoverflow.com/questions/29874150/working-with-evp-and-openssl-coding-in-c
|
||||
*
|
||||
* @param [in] plaintext Bytes to encrypt or decrypt
|
||||
* @param [in] plaintext_len Size of plaintext
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] encode 1 or encrypt 0 for decrypt
|
||||
* @param [in] mode EVP_CIPH_ECB_MODE or EVP_CIPH_CBC_MODE
|
||||
* @param [out] ciphertext_len The bytes written
|
||||
* @return Array of char with data encrypted or decrypted
|
||||
*/
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data) {
|
||||
char *fish_cipher(const char *plaintext, size_t plaintext_len, const char *key, size_t keylen, int encode, int mode, size_t *ciphertext_len) {
|
||||
EVP_CIPHER_CTX *ctx;
|
||||
EVP_CIPHER *cipher = NULL;
|
||||
int bytes_written = 0;
|
||||
unsigned char *ciphertext = NULL;
|
||||
unsigned char *iv_ciphertext = NULL;
|
||||
unsigned char *iv = NULL;
|
||||
size_t block_size = 0;
|
||||
|
||||
*ciphertext_len = 0;
|
||||
|
||||
if (plaintext_len == 0 || keylen == 0 || encode < 0 || encode > 1)
|
||||
return NULL;
|
||||
|
||||
block_size = plaintext_len;
|
||||
|
||||
if (mode == EVP_CIPH_CBC_MODE) {
|
||||
if (encode == 1) {
|
||||
iv = (unsigned char *) g_malloc0(8);
|
||||
RAND_bytes(iv, 8);
|
||||
} else {
|
||||
if (plaintext_len <= 8) /* IV + DATA */
|
||||
return NULL;
|
||||
|
||||
iv = (unsigned char *) plaintext;
|
||||
block_size -= 8;
|
||||
plaintext += 8;
|
||||
plaintext_len -= 8;
|
||||
}
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
cipher = EVP_CIPHER_fetch(ossl_ctx, "BF-CBC", NULL);
|
||||
#else
|
||||
cipher = (EVP_CIPHER *) EVP_bf_cbc();
|
||||
#endif
|
||||
|
||||
} else if (mode == EVP_CIPH_ECB_MODE) {
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
cipher = EVP_CIPHER_fetch(ossl_ctx, "BF-ECB", NULL);
|
||||
#else
|
||||
cipher = (EVP_CIPHER *) EVP_bf_ecb();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Zero Padding */
|
||||
if (block_size % 8 != 0) {
|
||||
block_size = block_size + 8 - (block_size % 8);
|
||||
}
|
||||
|
||||
ciphertext = (unsigned char *) g_malloc0(block_size);
|
||||
memcpy(ciphertext, plaintext, plaintext_len);
|
||||
|
||||
/* Create and initialise the context */
|
||||
if (!(ctx = EVP_CIPHER_CTX_new()))
|
||||
return NULL;
|
||||
|
||||
/* Initialise the cipher operation only with mode */
|
||||
if (!EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encode))
|
||||
return NULL;
|
||||
|
||||
/* Set custom key length */
|
||||
if (!EVP_CIPHER_CTX_set_key_length(ctx, keylen))
|
||||
return NULL;
|
||||
|
||||
/* Finish the initiation the cipher operation */
|
||||
if (1 != EVP_CipherInit_ex(ctx, NULL, NULL, (const unsigned char *) key, iv, encode))
|
||||
return NULL;
|
||||
|
||||
/* We will manage this */
|
||||
EVP_CIPHER_CTX_set_padding(ctx, 0);
|
||||
|
||||
/* Do cipher operation */
|
||||
if (1 != EVP_CipherUpdate(ctx, ciphertext, &bytes_written, ciphertext, block_size))
|
||||
return NULL;
|
||||
|
||||
*ciphertext_len = bytes_written;
|
||||
|
||||
/* Finalise the cipher. Further ciphertext bytes may be written at this stage */
|
||||
if (1 != EVP_CipherFinal_ex(ctx, ciphertext + bytes_written, &bytes_written))
|
||||
return NULL;
|
||||
|
||||
*ciphertext_len += bytes_written;
|
||||
|
||||
/* Clean up */
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
|
||||
if (mode == EVP_CIPH_CBC_MODE && encode == 1) {
|
||||
/* Join IV + DATA */
|
||||
iv_ciphertext = g_malloc0(8 + *ciphertext_len);
|
||||
|
||||
memcpy(iv_ciphertext, iv, 8);
|
||||
memcpy(&iv_ciphertext[8], ciphertext, *ciphertext_len);
|
||||
*ciphertext_len += 8;
|
||||
|
||||
g_free(ciphertext);
|
||||
g_free(iv);
|
||||
|
||||
return (char *) iv_ciphertext;
|
||||
} else {
|
||||
return (char *) ciphertext;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a fish or standard Base64 encoded string with data encrypted
|
||||
* is binary safe
|
||||
*
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] message Bytes to encrypt
|
||||
* @param [in] message_len Size of message
|
||||
* @param [in] mode Chiper mode
|
||||
* @return Array of char with data encrypted
|
||||
*/
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode) {
|
||||
size_t ciphertext_len = 0;
|
||||
char *ciphertext = NULL;
|
||||
char *b64 = NULL;
|
||||
|
||||
if (keylen == 0 || message_len == 0)
|
||||
return NULL;
|
||||
|
||||
ciphertext = fish_cipher(message, message_len, key, keylen, 1, mode, &ciphertext_len);
|
||||
|
||||
if (ciphertext == NULL || ciphertext_len == 0)
|
||||
return NULL;
|
||||
|
||||
switch (mode) {
|
||||
case FISH_CBC_MODE:
|
||||
b64 = g_base64_encode((const unsigned char *) ciphertext, ciphertext_len);
|
||||
break;
|
||||
|
||||
case FISH_ECB_MODE:
|
||||
b64 = fish_base64_encode((const char *) ciphertext, ciphertext_len);
|
||||
}
|
||||
|
||||
g_free(ciphertext);
|
||||
|
||||
if (b64 == NULL)
|
||||
return NULL;
|
||||
|
||||
return b64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of bytes with data decrypted
|
||||
* is binary safe
|
||||
*
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] data Fish or standard Base64 encoded string
|
||||
* @param [in] mode Chiper mode
|
||||
* @param [out] final_len Length of returned array
|
||||
* @return Array of char with data decrypted
|
||||
*/
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len) {
|
||||
size_t ciphertext_len = 0;
|
||||
char *ciphertext = NULL;
|
||||
char *plaintext = NULL;
|
||||
|
||||
*final_len = 0;
|
||||
|
||||
if (keylen == 0 || strlen(data) == 0)
|
||||
return NULL;
|
||||
|
||||
switch (mode) {
|
||||
case FISH_CBC_MODE:
|
||||
if (strspn(data, base64_chars) != strlen(data))
|
||||
return NULL;
|
||||
ciphertext = (char *) g_base64_decode(data, &ciphertext_len);
|
||||
break;
|
||||
|
||||
case FISH_ECB_MODE:
|
||||
ciphertext = fish_base64_decode(data, &ciphertext_len);
|
||||
}
|
||||
|
||||
if (ciphertext == NULL || ciphertext_len == 0)
|
||||
return NULL;
|
||||
|
||||
plaintext = fish_cipher(ciphertext, ciphertext_len, key, keylen, 0, mode, final_len);
|
||||
g_free(ciphertext);
|
||||
|
||||
if (*final_len == 0)
|
||||
return NULL;
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to fish_decrypt, but pad with zeros any after the first zero in the decrypted data
|
||||
*
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] data Fish or standard Base64 encoded string
|
||||
* @param [in] mode Chiper mode
|
||||
* @return Array of char with data decrypted
|
||||
*/
|
||||
char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode) {
|
||||
char *decrypted = NULL;
|
||||
char *plaintext_str = NULL;
|
||||
size_t decrypted_len = 0;
|
||||
|
||||
decrypted = fish_decrypt(key, strlen(key), data, mode, &decrypted_len);
|
||||
|
||||
if (decrypted == NULL || decrypted_len == 0)
|
||||
return NULL;
|
||||
|
||||
plaintext_str = g_strndup(decrypted, decrypted_len);
|
||||
g_free(decrypted);
|
||||
|
||||
return plaintext_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a nick have a key
|
||||
*
|
||||
* @param [in] nick Nickname
|
||||
* @return TRUE if have a key or FALSE if not
|
||||
*/
|
||||
gboolean fish_nick_has_key(const char *nick) {
|
||||
gboolean has_key = FALSE;
|
||||
char *key;
|
||||
enum fish_mode mode;
|
||||
|
||||
key = keystore_get_key(nick, &mode);
|
||||
if (key) {
|
||||
has_key = TRUE;
|
||||
g_free(key);
|
||||
};
|
||||
|
||||
return has_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a message (see fish_encrypt). The key is searched for in the key store
|
||||
*
|
||||
* @param [in] nick Nickname
|
||||
* @param [in] data Plaintext to encrypt
|
||||
* @param [out] omode Mode of encryption
|
||||
* @param [in] command_len Length of command to use without the message part
|
||||
* @return A list of encoded strings with the message encrypted or NULL if any error occurred
|
||||
*/
|
||||
GSList *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode, size_t command_len) {
|
||||
char *key;
|
||||
GSList *encrypted_list = NULL;
|
||||
char *encrypted = NULL;
|
||||
enum fish_mode mode;
|
||||
int max_len, max_chunks_len, chunks_len;
|
||||
|
||||
/* Look for key */
|
||||
key = keystore_get_key(nick, &mode);
|
||||
if (!key) return NULL;
|
||||
|
||||
*omode = mode;
|
||||
|
||||
/* Calculate max length of each line */
|
||||
max_len = MAX_COMMAND_LENGTH - command_len;
|
||||
/* Add '*' */
|
||||
if (mode == FISH_CBC_MODE) max_len--;
|
||||
|
||||
max_chunks_len = max_text_command_len(max_len, mode);
|
||||
|
||||
const char *data_chunk = data;
|
||||
|
||||
while(foreach_utf8_data_chunks(data_chunk, max_chunks_len, &chunks_len)) {
|
||||
encrypted = fish_encrypt(key, strlen(key), data_chunk, chunks_len, mode);
|
||||
|
||||
if (mode == FISH_CBC_MODE) {
|
||||
/* Add '*' for CBC */
|
||||
encrypted_list = g_slist_append(encrypted_list, g_strdup_printf("*%s", encrypted));
|
||||
g_free(encrypted);
|
||||
} else {
|
||||
encrypted_list = g_slist_append(encrypted_list, encrypted);
|
||||
}
|
||||
|
||||
/* Next chunk */
|
||||
data_chunk += chunks_len;
|
||||
}
|
||||
|
||||
return encrypted_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a message (see fish_decrypt). The key is searched for in the key store
|
||||
*
|
||||
* @param [in] nick Nickname
|
||||
* @param [in] data Plaintext to encrypt
|
||||
* @param [out] omode Mode of encryption
|
||||
* @return Plaintext message or NULL if any error occurred
|
||||
*/
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode) {
|
||||
char *key;
|
||||
char *decrypted;
|
||||
enum fish_mode mode;
|
||||
|
||||
/* Look for key */
|
||||
key = keystore_get_key(nick);
|
||||
key = keystore_get_key(nick, &mode);
|
||||
if (!key) return NULL;
|
||||
|
||||
|
||||
*omode = mode;
|
||||
|
||||
if (mode == FISH_CBC_MODE)
|
||||
++data;
|
||||
|
||||
/* Decrypt */
|
||||
decrypted = fish_decrypt(key, strlen(key), data);
|
||||
|
||||
decrypted = fish_decrypt_str(key, strlen(key), data, mode);
|
||||
g_free(key);
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -29,10 +30,21 @@
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message);
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data);
|
||||
char *fish_encrypt_for_nick(const char *nick, const char *data);
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data);
|
||||
enum fish_mode {
|
||||
FISH_ECB_MODE = 0x1,
|
||||
FISH_CBC_MODE = 0x2
|
||||
};
|
||||
|
||||
int fish_init(void);
|
||||
void fish_deinit(void);
|
||||
char *fish_base64_encode(const char *message, size_t message_len);
|
||||
char *fish_base64_decode(const char *message, size_t *final_len);
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode);
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len);
|
||||
char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode);
|
||||
gboolean fish_nick_has_key(const char *nick);
|
||||
GSList *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode, size_t command_len);
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode);
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@ -29,7 +29,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;HAVE_DH_SET0_PQG;HAVE_DH_GET0_KEY;HAVE_DH_SET0_KEY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(Glib);..\..\src\common;$(HexChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@ -40,7 +40,7 @@
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<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;$(Glib);..\..\src\common;$(HexChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@ -55,6 +55,7 @@
|
||||
<ItemGroup>
|
||||
<ClInclude Include="dh1080.h" />
|
||||
<ClInclude Include="fish.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
<ClInclude Include="irc.h" />
|
||||
<ClInclude Include="keystore.h" />
|
||||
<ClInclude Include="plugin_hexchat.h" />
|
||||
@ -62,6 +63,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dh1080.c" />
|
||||
<ClCompile Include="fish.c" />
|
||||
<ClCompile Include="utils.c" />
|
||||
<ClCompile Include="irc.c" />
|
||||
<ClCompile Include="keystore.c" />
|
||||
<ClCompile Include="plugin_hexchat.c" />
|
||||
|
@ -23,9 +23,15 @@
|
||||
<ClInclude Include="bool.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="dh1080.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="fish.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="utils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="irc.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@ -37,9 +43,15 @@
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dh1080.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="fish.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="utils.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="irc.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
@ -103,27 +103,55 @@ static gchar *get_nick_value(GKeyFile *keyfile, const char *nick, const char *it
|
||||
/**
|
||||
* Extracts a key from the key store file.
|
||||
*/
|
||||
char *keystore_get_key(const char *nick) {
|
||||
char *keystore_get_key(const char *nick, enum fish_mode *mode) {
|
||||
GKeyFile *keyfile;
|
||||
char *escaped_nick;
|
||||
gchar *value, *key_mode;
|
||||
int encrypted_mode;
|
||||
char *password;
|
||||
char *encrypted;
|
||||
char *decrypted;
|
||||
|
||||
/* Get the key */
|
||||
GKeyFile *keyfile = getConfigFile();
|
||||
char *escaped_nick = escape_nickname(nick);
|
||||
gchar *value = get_nick_value(keyfile, escaped_nick, "key");
|
||||
keyfile = getConfigFile();
|
||||
escaped_nick = escape_nickname(nick);
|
||||
value = get_nick_value(keyfile, escaped_nick, "key");
|
||||
key_mode = get_nick_value(keyfile, escaped_nick, "mode");
|
||||
g_key_file_free(keyfile);
|
||||
g_free(escaped_nick);
|
||||
|
||||
/* Determine cipher mode */
|
||||
*mode = FISH_ECB_MODE;
|
||||
if (key_mode) {
|
||||
if (*key_mode == '1')
|
||||
*mode = FISH_ECB_MODE;
|
||||
else if (*key_mode == '2')
|
||||
*mode = FISH_CBC_MODE;
|
||||
g_free(key_mode);
|
||||
}
|
||||
|
||||
if (!value)
|
||||
return NULL;
|
||||
|
||||
if (strncmp(value, "+OK ", 4) != 0) {
|
||||
/* Key is stored in plaintext */
|
||||
return value;
|
||||
} else {
|
||||
|
||||
if (strncmp(value, "+OK ", 4) == 0) {
|
||||
/* Key is encrypted */
|
||||
const char *encrypted = value+4;
|
||||
const char *password = get_keystore_password();
|
||||
char *decrypted = fish_decrypt(password, strlen(password), encrypted);
|
||||
encrypted = (char *) value;
|
||||
encrypted += 4;
|
||||
|
||||
encrypted_mode = FISH_ECB_MODE;
|
||||
|
||||
if (*encrypted == '*') {
|
||||
++encrypted;
|
||||
encrypted_mode = FISH_CBC_MODE;
|
||||
}
|
||||
|
||||
password = (char *) get_keystore_password();
|
||||
decrypted = fish_decrypt_str((const char *) password, strlen(password), (const char *) encrypted, encrypted_mode);
|
||||
g_free(value);
|
||||
return decrypted;
|
||||
} else {
|
||||
/* Key is stored in plaintext */
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +217,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
/**
|
||||
* Sets a key in the key store file.
|
||||
*/
|
||||
gboolean keystore_store_key(const char *nick, const char *key) {
|
||||
gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode) {
|
||||
const char *password;
|
||||
char *encrypted;
|
||||
char *wrapped;
|
||||
@ -204,11 +232,11 @@ gboolean keystore_store_key(const char *nick, const char *key) {
|
||||
password = get_keystore_password();
|
||||
if (password) {
|
||||
/* Encrypt the password */
|
||||
encrypted = fish_encrypt(password, strlen(password), key);
|
||||
encrypted = fish_encrypt(password, strlen(password), key, strlen(key), FISH_CBC_MODE);
|
||||
if (!encrypted) goto end;
|
||||
|
||||
/* Prepend "+OK " */
|
||||
wrapped = g_strconcat("+OK ", encrypted, NULL);
|
||||
wrapped = g_strconcat("+OK *", encrypted, NULL);
|
||||
g_free(encrypted);
|
||||
|
||||
/* Store encrypted in file */
|
||||
@ -218,6 +246,9 @@ gboolean keystore_store_key(const char *nick, const char *key) {
|
||||
/* Store unencrypted in file */
|
||||
g_key_file_set_string(keyfile, escaped_nick, "key", key);
|
||||
}
|
||||
|
||||
/* Store cipher mode */
|
||||
g_key_file_set_integer(keyfile, escaped_nick, "mode", mode);
|
||||
|
||||
/* Save key store file */
|
||||
ok = save_keystore(keyfile);
|
||||
|
@ -28,9 +28,10 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#include <glib.h>
|
||||
#include "fish.h"
|
||||
|
||||
char *keystore_get_key(const char *nick);
|
||||
gboolean keystore_store_key(const char *nick, const char *key);
|
||||
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);
|
||||
|
||||
#endif
|
||||
|
@ -2,9 +2,13 @@ if not libssl_dep.found()
|
||||
error('fish plugin requires openssl')
|
||||
endif
|
||||
|
||||
# Run tests
|
||||
subdir('tests')
|
||||
|
||||
fishlim_sources = [
|
||||
'dh1080.c',
|
||||
'fish.c',
|
||||
'utils.c',
|
||||
'irc.c',
|
||||
'keystore.c',
|
||||
'plugin_hexchat.c'
|
||||
@ -15,4 +19,5 @@ shared_module('fishlim', fishlim_sources,
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'fishlim.def',
|
||||
)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
Copyright (c) 2010-2011 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
Copyright (c) 2015 <the.cypher@gmail.com>
|
||||
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -28,21 +29,21 @@
|
||||
#include <glib.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hexchat-plugin.h"
|
||||
#define HEXCHAT_MAX_WORDS 32
|
||||
|
||||
#include "fish.h"
|
||||
#include "dh1080.h"
|
||||
#include "keystore.h"
|
||||
#include "irc.h"
|
||||
|
||||
static const char *fish_modes[] = {"", "ECB", "CBC", NULL};
|
||||
|
||||
static const char plugin_name[] = "FiSHLiM";
|
||||
static const char plugin_desc[] = "Encryption plugin for the FiSH protocol. Less is More!";
|
||||
static const char plugin_version[] = "0.1.0";
|
||||
static const char plugin_version[] = "1.0.0";
|
||||
|
||||
static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] <password>, sets the key for a channel or nick";
|
||||
static const char usage_delkey[] = "Usage: DELKEY <nick or #channel>, deletes the key for a channel or nick";
|
||||
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_topic[] = "Usage: TOPIC+ <topic>, sets a new encrypted topic for the current channel";
|
||||
static const char usage_notice[] = "Usage: NOTICE+ <nick or #channel> <notice>";
|
||||
@ -53,6 +54,13 @@ static hexchat_plugin *ph;
|
||||
static GHashTable *pending_exchanges;
|
||||
|
||||
|
||||
/**
|
||||
* Compare two nicks using the current plugin
|
||||
*/
|
||||
int irc_nick_cmp(const char *a, const char *b) {
|
||||
return hexchat_nickcmp (ph, a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the key store file.
|
||||
*/
|
||||
@ -88,7 +96,7 @@ static hexchat_context *find_context_on_network (const char *name) {
|
||||
int chan_id = hexchat_list_int(ph, channels, "id");
|
||||
const char *chan_name = hexchat_list_str(ph, channels, "channel");
|
||||
|
||||
if (chan_id == id && chan_name && hexchat_nickcmp (ph, chan_name, name) == 0) {
|
||||
if (chan_id == id && chan_name && irc_nick_cmp (chan_name, name) == 0) {
|
||||
ret = (hexchat_context*)hexchat_list_str(ph, channels, "context");
|
||||
break;
|
||||
}
|
||||
@ -98,8 +106,175 @@ static hexchat_context *find_context_on_network (const char *name) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
int irc_nick_cmp(const char *a, const char *b) {
|
||||
return hexchat_nickcmp (ph, a, b);
|
||||
/**
|
||||
* Retrive the field for own user in current context
|
||||
* @return the field value
|
||||
*/
|
||||
char *get_my_info(const char *field, gboolean find_in_other_context) {
|
||||
char *result = NULL;
|
||||
const char *own_nick;
|
||||
hexchat_list *list;
|
||||
hexchat_context *ctx_current, *ctx_channel;
|
||||
|
||||
/* Display message */
|
||||
own_nick = hexchat_get_info(ph, "nick");
|
||||
|
||||
if (!own_nick)
|
||||
return NULL;
|
||||
|
||||
/* Get field for own nick if any */
|
||||
list = hexchat_list_get(ph, "users");
|
||||
if (list) {
|
||||
while (hexchat_list_next(ph, list)) {
|
||||
if (irc_nick_cmp(own_nick, hexchat_list_str(ph, list, "nick")) == 0)
|
||||
result = g_strdup(hexchat_list_str(ph, list, field));
|
||||
}
|
||||
hexchat_list_free(ph, list);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Try to get from a channel (we are outside a channel) */
|
||||
if (!find_in_other_context) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
list = hexchat_list_get(ph, "channels");
|
||||
if (list) {
|
||||
ctx_current = hexchat_get_context(ph);
|
||||
while (hexchat_list_next(ph, list)) {
|
||||
ctx_channel = (hexchat_context *) hexchat_list_str(ph, list, "context");
|
||||
|
||||
hexchat_set_context(ph, ctx_channel);
|
||||
result = get_my_info(field, FALSE);
|
||||
hexchat_set_context(ph, ctx_current);
|
||||
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
hexchat_list_free(ph, list);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive the prefix character for own nick in current context
|
||||
* @return @ or + or NULL
|
||||
*/
|
||||
char *get_my_own_prefix(void) {
|
||||
return get_my_info("prefix", FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive the mask for own nick in current context
|
||||
* @return Host name in the form: user@host (or NULL if not known)
|
||||
*/
|
||||
char *get_my_own_host(void) {
|
||||
return get_my_info("host", TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of prefix for current user in current context
|
||||
*
|
||||
* @return Length of prefix
|
||||
*/
|
||||
int get_prefix_length(void) {
|
||||
char *own_host;
|
||||
int prefix_len = 0;
|
||||
|
||||
/* ':! ' + 'nick' + 'ident@host', e.g. ':user!~name@mynet.com ' */
|
||||
prefix_len = 3 + strlen(hexchat_get_info(ph, "nick"));
|
||||
own_host = get_my_own_host();
|
||||
if (own_host) {
|
||||
prefix_len += strlen(own_host);
|
||||
} else {
|
||||
/* https://stackoverflow.com/questions/8724954/what-is-the-maximum-number-of-characters-for-a-host-name-in-unix */
|
||||
prefix_len += 64;
|
||||
}
|
||||
g_free(own_host);
|
||||
|
||||
return prefix_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to decrypt the first occurrence of fish message
|
||||
*
|
||||
* @param message Message to decrypt
|
||||
* @param key Key of message
|
||||
* @return Array of char with decrypted message or NULL. The returned string
|
||||
* should be freed with g_free() when no longer needed.
|
||||
*/
|
||||
char *decrypt_raw_message(const char *message, const char *key) {
|
||||
const char *prefixes[] = {"+OK ", "mcps ", NULL};
|
||||
char *start = NULL, *end = NULL;
|
||||
char *left = NULL, *right = NULL;
|
||||
char *encrypted = NULL, *decrypted = NULL;
|
||||
int length = 0;
|
||||
int index_prefix;
|
||||
enum fish_mode mode;
|
||||
GString *message_decrypted;
|
||||
char *result = NULL;
|
||||
|
||||
if (message == NULL || key == NULL)
|
||||
return NULL;
|
||||
|
||||
for (index_prefix = 0; index_prefix < 2; index_prefix++) {
|
||||
start = g_strstr_len(message, strlen(message), prefixes[index_prefix]);
|
||||
if (start) {
|
||||
/* Length ALWAYS will be less that original message
|
||||
* add '[CBC] ' length */
|
||||
message_decrypted = g_string_sized_new(strlen(message) + 6);
|
||||
|
||||
/* Left part of message */
|
||||
left = g_strndup(message, start - message);
|
||||
g_string_append(message_decrypted, left);
|
||||
g_free(left);
|
||||
|
||||
/* Encrypted part */
|
||||
start += strlen(prefixes[index_prefix]);
|
||||
end = g_strstr_len(start, strlen(message), " ");
|
||||
if (end) {
|
||||
length = end - start;
|
||||
right = end;
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
encrypted = g_strndup(start, length);
|
||||
} else {
|
||||
encrypted = g_strdup(start);
|
||||
}
|
||||
decrypted = fish_decrypt_from_nick(key, encrypted, &mode);
|
||||
g_free(encrypted);
|
||||
|
||||
if (decrypted == NULL) {
|
||||
g_string_free(message_decrypted, TRUE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Add encrypted flag */
|
||||
g_string_append(message_decrypted, "[");
|
||||
g_string_append(message_decrypted, fish_modes[mode]);
|
||||
g_string_append(message_decrypted, "] ");
|
||||
/* Decrypted message */
|
||||
g_string_append(message_decrypted, decrypted);
|
||||
g_free(decrypted);
|
||||
|
||||
/* Right part of message */
|
||||
if (right) {
|
||||
g_string_append(message_decrypted, right);
|
||||
}
|
||||
|
||||
result = message_decrypted->str;
|
||||
g_string_free(message_decrypted, FALSE);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*static int handle_debug(char *word[], char *word_eol[], void *userdata) {
|
||||
@ -115,20 +290,49 @@ int irc_nick_cmp(const char *a, const char *b) {
|
||||
* Called when a message is to be sent.
|
||||
*/
|
||||
static int handle_outgoing(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *own_nick;
|
||||
/* Encrypt the message if possible */
|
||||
char *prefix;
|
||||
enum fish_mode mode;
|
||||
char *message;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
const char *channel = hexchat_get_info(ph, "channel");
|
||||
char *encrypted = fish_encrypt_for_nick(channel, word_eol[1]);
|
||||
if (!encrypted) return HEXCHAT_EAT_NONE;
|
||||
|
||||
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(channel)) return HEXCHAT_EAT_NONE;
|
||||
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "PRIVMSG %s :+OK ", channel);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(channel, word_eol[1], &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
return HEXCHAT_EAT_NONE;
|
||||
}
|
||||
|
||||
/* Get prefix for own nick if any */
|
||||
prefix = get_my_own_prefix();
|
||||
|
||||
/* Add encrypted flag */
|
||||
message = g_strconcat("[", fish_modes[mode], "] ", word_eol[1], NULL);
|
||||
|
||||
/* Display message */
|
||||
own_nick = hexchat_get_info(ph, "nick");
|
||||
hexchat_emit_print(ph, "Your Message", own_nick, word_eol[1], NULL);
|
||||
|
||||
/* Send message */
|
||||
hexchat_commandf(ph, "PRIVMSG %s :+OK %s", channel, encrypted);
|
||||
|
||||
g_free(encrypted);
|
||||
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), message, prefix, NULL);
|
||||
g_free(message);
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item)
|
||||
{
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *)encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_free(prefix);
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
|
||||
@ -139,96 +343,59 @@ static int handle_incoming(char *word[], char *word_eol[], hexchat_event_attrs *
|
||||
const char *prefix;
|
||||
const char *command;
|
||||
const char *recipient;
|
||||
const char *encrypted;
|
||||
const char *peice;
|
||||
const char *raw_message = word_eol[1];
|
||||
char *sender_nick;
|
||||
char *decrypted;
|
||||
size_t w;
|
||||
size_t ew;
|
||||
size_t uw;
|
||||
char prefix_char = 0;
|
||||
size_t parameters_offset;
|
||||
GString *message;
|
||||
|
||||
if (!irc_parse_message((const char **)word, &prefix, &command, &w))
|
||||
if (!irc_parse_message((const char **)word, &prefix, &command, ¶meters_offset))
|
||||
return HEXCHAT_EAT_NONE;
|
||||
|
||||
|
||||
/* Topic (command 332) has an extra parameter */
|
||||
if (!strcmp(command, "332")) w++;
|
||||
|
||||
/* Look for encrypted data */
|
||||
for (ew = w+1; ew < HEXCHAT_MAX_WORDS-1; ew++) {
|
||||
const char *s = (ew == w+1 ? word[ew]+1 : word[ew]);
|
||||
if (*s && (s[1] == '+' || s[1] == 'm')) { prefix_char = *(s++); }
|
||||
else { prefix_char = 0; }
|
||||
if (strcmp(s, "+OK") == 0 || strcmp(s, "mcps") == 0) goto has_encrypted_data;
|
||||
if (!strcmp(command, "332"))
|
||||
parameters_offset++;
|
||||
|
||||
/* Extract sender nick and recipient nick/channel and try to decrypt */
|
||||
recipient = word[parameters_offset];
|
||||
decrypted = decrypt_raw_message(raw_message, recipient);
|
||||
if (decrypted == NULL) {
|
||||
sender_nick = irc_prefix_get_nick(prefix);
|
||||
decrypted = decrypt_raw_message(raw_message, sender_nick);
|
||||
g_free(sender_nick);
|
||||
}
|
||||
return HEXCHAT_EAT_NONE;
|
||||
has_encrypted_data: ;
|
||||
/* Extract sender nick and recipient nick/channel */
|
||||
sender_nick = irc_prefix_get_nick(prefix);
|
||||
recipient = word[w];
|
||||
|
||||
/* Try to decrypt with these (the keys are searched for in the key store) */
|
||||
encrypted = word[ew+1];
|
||||
decrypted = fish_decrypt_from_nick(recipient, encrypted);
|
||||
if (!decrypted) decrypted = fish_decrypt_from_nick(sender_nick, encrypted);
|
||||
|
||||
/* Check for error */
|
||||
if (!decrypted) goto decrypt_error;
|
||||
|
||||
/* Build unecrypted message */
|
||||
message = g_string_sized_new (100); /* TODO: more accurate estimation of size */
|
||||
g_string_append (message, "RECV");
|
||||
|
||||
/* Nothing to decrypt */
|
||||
if (decrypted == NULL)
|
||||
return HEXCHAT_EAT_NONE;
|
||||
|
||||
/* Build decrypted message */
|
||||
|
||||
/* decrypted + 'RECV ' + '@time=YYYY-MM-DDTHH:MM:SS.fffffZ ' */
|
||||
message = g_string_sized_new (strlen(decrypted) + 5 + 33);
|
||||
g_string_append (message, "RECV ");
|
||||
|
||||
if (attrs->server_time_utc)
|
||||
{
|
||||
GTimeVal tv = { (glong)attrs->server_time_utc, 0 };
|
||||
char *timestamp = g_time_val_to_iso8601 (&tv);
|
||||
|
||||
g_string_append (message, " @time=");
|
||||
g_string_append (message, "@time=");
|
||||
g_string_append (message, timestamp);
|
||||
g_string_append (message, " ");
|
||||
g_free (timestamp);
|
||||
}
|
||||
|
||||
for (uw = 1; uw < HEXCHAT_MAX_WORDS; uw++) {
|
||||
if (word[uw][0] != '\0')
|
||||
g_string_append_c (message, ' ');
|
||||
|
||||
if (uw == ew) {
|
||||
/* Add the encrypted data */
|
||||
peice = decrypted;
|
||||
uw++; /* Skip "OK+" */
|
||||
|
||||
if (ew == w+1) {
|
||||
/* Prefix with colon, which gets stripped out otherwise */
|
||||
g_string_append_c (message, ':');
|
||||
}
|
||||
|
||||
if (prefix_char) {
|
||||
g_string_append_c (message, prefix_char);
|
||||
}
|
||||
|
||||
} else {
|
||||
/* Add unencrypted data (for example, a prefix from a bouncer or bot) */
|
||||
peice = word[uw];
|
||||
}
|
||||
|
||||
g_string_append (message, peice);
|
||||
}
|
||||
g_string_append (message, decrypted);
|
||||
g_free(decrypted);
|
||||
|
||||
/* Simulate unencrypted message */
|
||||
/* hexchat_printf(ph, "simulating: %s\n", message->str); */
|
||||
/* Fake server message
|
||||
* RECV command will throw this function again, if message have multiple
|
||||
* encrypted data, we will decrypt all */
|
||||
hexchat_command(ph, message->str);
|
||||
|
||||
g_string_free (message, TRUE);
|
||||
g_free(sender_nick);
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
|
||||
decrypt_error:
|
||||
g_free(decrypted);
|
||||
g_free(sender_nick);
|
||||
return HEXCHAT_EAT_NONE;
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
|
||||
static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
@ -236,8 +403,8 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *dh_pubkey = word[5];
|
||||
hexchat_context *query_ctx;
|
||||
const char *prefix;
|
||||
gboolean cbc;
|
||||
char *sender, *secret_key, *priv_key = NULL;
|
||||
enum fish_mode mode = FISH_ECB_MODE;
|
||||
|
||||
if (!*dh_message || !*dh_pubkey || strlen(dh_pubkey) != 181)
|
||||
return HEXCHAT_EAT_NONE;
|
||||
@ -248,25 +415,19 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
sender = irc_prefix_get_nick(prefix);
|
||||
query_ctx = find_context_on_network(sender);
|
||||
if (query_ctx)
|
||||
hexchat_set_context(ph, query_ctx);
|
||||
g_assert(hexchat_set_context(ph, query_ctx) == 1);
|
||||
|
||||
dh_message++; /* : prefix */
|
||||
if (*dh_message == '+' || *dh_message == '-')
|
||||
dh_message++; /* identify-msg */
|
||||
|
||||
cbc = g_strcmp0 (word[6], "CBC") == 0;
|
||||
if (g_strcmp0 (word[6], "CBC") == 0)
|
||||
mode = FISH_CBC_MODE;
|
||||
|
||||
if (!strcmp(dh_message, "DH1080_INIT")) {
|
||||
char *pub_key;
|
||||
|
||||
if (cbc) {
|
||||
hexchat_print(ph, "Received key exchange for CBC mode which is not supported.");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
hexchat_printf(ph, "Received public key from %s, sending mine...", sender);
|
||||
hexchat_printf(ph, "Received public key from %s (%s), sending mine...", sender, fish_modes[mode]);
|
||||
if (dh1080_generate_key(&priv_key, &pub_key)) {
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s", sender, pub_key);
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s%s", sender, pub_key, (mode == FISH_CBC_MODE) ? " CBC" : "");
|
||||
g_free(pub_key);
|
||||
} else {
|
||||
hexchat_print(ph, "Failed to generate keys");
|
||||
@ -279,11 +440,6 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
g_hash_table_steal(pending_exchanges, sender_lower);
|
||||
g_free(sender_lower);
|
||||
|
||||
if (cbc) {
|
||||
hexchat_print(ph, "Received key exchange for CBC mode which is not supported.");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!priv_key) {
|
||||
hexchat_printf(ph, "Received a key exchange response for unknown user: %s", sender);
|
||||
goto cleanup;
|
||||
@ -295,8 +451,8 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
}
|
||||
|
||||
if (dh1080_compute_key(priv_key, dh_pubkey, &secret_key)) {
|
||||
keystore_store_key(sender, secret_key);
|
||||
hexchat_printf(ph, "Stored new key for %s", sender);
|
||||
keystore_store_key(sender, secret_key, mode);
|
||||
hexchat_printf(ph, "Stored new key for %s (%s)", sender, fish_modes[mode]);
|
||||
g_free(secret_key);
|
||||
} else {
|
||||
hexchat_print(ph, "Failed to create secret key!");
|
||||
@ -314,6 +470,7 @@ cleanup:
|
||||
static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *nick;
|
||||
const char *key;
|
||||
enum fish_mode mode;
|
||||
|
||||
/* Check syntax */
|
||||
if (*word[2] == '\0') {
|
||||
@ -331,9 +488,17 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
key = word_eol[3];
|
||||
}
|
||||
|
||||
mode = FISH_ECB_MODE;
|
||||
if (g_ascii_strncasecmp("cbc:", key, 4) == 0) {
|
||||
key = key+4;
|
||||
mode = FISH_CBC_MODE;
|
||||
} else if (g_ascii_strncasecmp("ecb:", key, 4) == 0) {
|
||||
key = key+4;
|
||||
}
|
||||
|
||||
/* Set password */
|
||||
if (keystore_store_key(nick, key)) {
|
||||
hexchat_printf(ph, "Stored key for %s\n", nick);
|
||||
if (keystore_store_key(nick, key, mode)) {
|
||||
hexchat_printf(ph, "Stored key for %s (%s)\n", nick, fish_modes[mode]);
|
||||
} else {
|
||||
hexchat_printf(ph, "\00305Failed to store key in addon_fishlim.conf\n");
|
||||
}
|
||||
@ -345,22 +510,30 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
* Command handler for /delkey
|
||||
*/
|
||||
static int handle_delkey(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *nick;
|
||||
char *nick = NULL;
|
||||
int ctx_type = 0;
|
||||
|
||||
/* Check syntax */
|
||||
if (*word[2] == '\0' || *word[3] != '\0') {
|
||||
hexchat_printf(ph, "%s\n", usage_delkey);
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
/* Delete key from input */
|
||||
if (*word[2] != '\0') {
|
||||
nick = g_strstrip(g_strdup(word_eol[2]));
|
||||
} else { /* Delete key from current context */
|
||||
nick = g_strdup(hexchat_get_info(ph, "channel"));
|
||||
ctx_type = hexchat_list_int(ph, NULL, "type");
|
||||
|
||||
/* Only allow channel or dialog */
|
||||
if (ctx_type < 2 || ctx_type > 3) {
|
||||
hexchat_printf(ph, "%s\n", usage_delkey);
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
}
|
||||
|
||||
nick = g_strstrip (word_eol[2]);
|
||||
|
||||
/* Delete the given nick from the key store */
|
||||
if (keystore_delete_nick(nick)) {
|
||||
hexchat_printf(ph, "Deleted key for %s\n", nick);
|
||||
} else {
|
||||
hexchat_printf(ph, "\00305Failed to delete key in addon_fishlim.conf!\n");
|
||||
}
|
||||
g_free(nick);
|
||||
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
@ -379,7 +552,7 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
}
|
||||
|
||||
if (query_ctx) {
|
||||
hexchat_set_context(ph, query_ctx);
|
||||
g_assert(hexchat_set_context(ph, query_ctx) == 1);
|
||||
ctx_type = hexchat_list_int(ph, NULL, "type");
|
||||
}
|
||||
|
||||
@ -391,8 +564,8 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
if (dh1080_generate_key(&priv_key, &pub_key)) {
|
||||
g_hash_table_replace (pending_exchanges, g_ascii_strdown(target, -1), priv_key);
|
||||
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s", target, pub_key);
|
||||
hexchat_printf(ph, "Sent public key to %s, waiting for reply...", target);
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s CBC", target, pub_key);
|
||||
hexchat_printf(ph, "Sent public key to %s (CBC), waiting for reply...", target);
|
||||
|
||||
g_free(pub_key);
|
||||
} else {
|
||||
@ -408,7 +581,9 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target;
|
||||
const char *topic = word_eol[2];
|
||||
char *buf;
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list;
|
||||
|
||||
if (!*topic) {
|
||||
hexchat_print(ph, usage_topic);
|
||||
@ -421,40 +596,77 @@ static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) {
|
||||
}
|
||||
|
||||
target = hexchat_get_info(ph, "channel");
|
||||
buf = fish_encrypt_for_nick(target, topic);
|
||||
if (buf == NULL) {
|
||||
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(target)) {
|
||||
hexchat_printf(ph, "/topic+ error, no key found for %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "TOPIC %s +OK %s", target, buf);
|
||||
g_free(buf);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "TOPIC %s +OK ", target);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(target, topic, &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/topic+ error, can't encrypt %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_list->data);
|
||||
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler for /notice+
|
||||
*/
|
||||
static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata)
|
||||
{
|
||||
static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target = word[2];
|
||||
const char *notice = word_eol[3];
|
||||
char *buf;
|
||||
char *notice_flag;
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
if (!*target || !*notice) {
|
||||
hexchat_print(ph, usage_notice);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
buf = fish_encrypt_for_nick(target, notice);
|
||||
if (buf == NULL) {
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(target)) {
|
||||
hexchat_printf(ph, "/notice+ error, no key found for %s.", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "quote NOTICE %s :+OK %s", target, buf);
|
||||
hexchat_emit_print(ph, "Notice Sent", target, notice);
|
||||
g_free(buf);
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "quote NOTICE %s :+OK ", target);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(target, notice, &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/notice+ error, can't encrypt %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
notice_flag = g_strconcat("[", fish_modes[mode], "] ", notice, NULL);
|
||||
hexchat_emit_print(ph, "Notice Send", target, notice_flag);
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item) {
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_free(notice_flag);
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
@ -462,56 +674,102 @@ static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata)
|
||||
/**
|
||||
* Command handler for /msg+
|
||||
*/
|
||||
static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata)
|
||||
{
|
||||
static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target = word[2];
|
||||
const char *message = word_eol[3];
|
||||
char *message_flag;
|
||||
char *prefix;
|
||||
hexchat_context *query_ctx;
|
||||
char *buf;
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
if (!*target || !*message) {
|
||||
hexchat_print(ph, usage_msg);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
buf = fish_encrypt_for_nick(target, message);
|
||||
if (buf == NULL) {
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(target)) {
|
||||
hexchat_printf(ph, "/msg+ error, no key found for %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "PRIVMSG %s :+OK %s", target, buf);
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "PRIVMSG %s :+OK ", target);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(target, message, &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/msg+ error, can't encrypt %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item) {
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
query_ctx = find_context_on_network(target);
|
||||
if (query_ctx) {
|
||||
hexchat_set_context(ph, query_ctx);
|
||||
g_assert(hexchat_set_context(ph, query_ctx) == 1);
|
||||
|
||||
/* FIXME: Mode char */
|
||||
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"),
|
||||
message, "", "");
|
||||
}
|
||||
else {
|
||||
prefix = get_my_own_prefix();
|
||||
|
||||
/* Add encrypted flag */
|
||||
message_flag = g_strconcat("[", fish_modes[mode], "] ", message, NULL);
|
||||
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), message_flag, prefix, NULL);
|
||||
g_free(prefix);
|
||||
g_free(message_flag);
|
||||
} else {
|
||||
hexchat_emit_print(ph, "Message Send", target, message);
|
||||
}
|
||||
|
||||
g_free(buf);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
static int handle_crypt_me(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *channel = hexchat_get_info(ph, "channel");
|
||||
char *buf;
|
||||
const char *channel = hexchat_get_info(ph, "channel");
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
buf = fish_encrypt_for_nick(channel, word_eol[2]);
|
||||
if (!buf)
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(channel)) {
|
||||
return HEXCHAT_EAT_NONE;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "PRIVMSG %s :\001ACTION +OK %s \001", channel, buf);
|
||||
hexchat_emit_print(ph, "Your Action", hexchat_get_info(ph, "nick"),
|
||||
word_eol[2], NULL);
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "PRIVMSG %s :\001ACTION +OK ", channel);
|
||||
|
||||
g_free(buf);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
/* 2 = ' \001' */
|
||||
encrypted_list = fish_encrypt_for_nick(channel, word_eol[2], &mode, get_prefix_length() + command->len + 2);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/me error, can't encrypt %s", channel);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_emit_print(ph, "Your Action", hexchat_get_info(ph, "nick"), word_eol[2], NULL);
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item) {
|
||||
hexchat_commandf(ph, "%s%s \001", command->str, (char *) encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -557,6 +815,9 @@ int hexchat_plugin_init(hexchat_plugin *plugin_handle,
|
||||
hexchat_hook_server_attrs(ph, "TOPIC", HEXCHAT_PRI_NORM, handle_incoming, NULL);
|
||||
hexchat_hook_server_attrs(ph, "332", HEXCHAT_PRI_NORM, handle_incoming, NULL);
|
||||
|
||||
if (!fish_init())
|
||||
return 0;
|
||||
|
||||
if (!dh1080_init())
|
||||
return 0;
|
||||
|
||||
@ -570,6 +831,7 @@ int hexchat_plugin_init(hexchat_plugin *plugin_handle,
|
||||
int hexchat_plugin_deinit(void) {
|
||||
g_clear_pointer(&pending_exchanges, g_hash_table_destroy);
|
||||
dh1080_deinit();
|
||||
fish_deinit();
|
||||
|
||||
hexchat_printf(ph, "%s plugin unloaded\n", plugin_name);
|
||||
return 1;
|
||||
|
16
plugins/fishlim/tests/meson.build
Normal file
16
plugins/fishlim/tests/meson.build
Normal file
@ -0,0 +1,16 @@
|
||||
fishlim_test_sources = [
|
||||
'tests.c',
|
||||
'mock-keystore.c',
|
||||
'../fish.c',
|
||||
'../utils.c',
|
||||
]
|
||||
|
||||
fishlim_tests = executable('fishlim_tests', fishlim_test_sources,
|
||||
dependencies: [libgio_dep, libssl_dep, hexchat_plugin_dep],
|
||||
include_directories: include_directories('..'),
|
||||
)
|
||||
|
||||
test('Fishlim Tests', fishlim_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 600,
|
||||
)
|
51
plugins/fishlim/tests/mock-keystore.c
Normal file
51
plugins/fishlim/tests/mock-keystore.c
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#include "fish.h"
|
||||
|
||||
/**
|
||||
* Extracts a key from the key store file.
|
||||
*/
|
||||
char *
|
||||
keystore_get_key(const char *nick, enum fish_mode *mode)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a key in the key store file.
|
||||
*/
|
||||
gboolean
|
||||
keystore_store_key(const char *nick, const char *key, enum fish_mode mode)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a nick from the key store.
|
||||
*/
|
||||
gboolean
|
||||
keystore_delete_nick(const char *nick)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
285
plugins/fishlim/tests/tests.c
Normal file
285
plugins/fishlim/tests/tests.c
Normal file
@ -0,0 +1,285 @@
|
||||
/*
|
||||
Copyright (c) 2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <glib.h>
|
||||
|
||||
#include "fish.h"
|
||||
#include "utils.h"
|
||||
|
||||
/**
|
||||
* Auxiliary function: Generate a random string
|
||||
* @param out Preallocated string to fill
|
||||
* @param len Size of bytes to fill
|
||||
*/
|
||||
static void
|
||||
random_string(char *out, size_t len)
|
||||
{
|
||||
GRand *rand = NULL;
|
||||
int i = 0;
|
||||
|
||||
rand = g_rand_new();
|
||||
for (i = 0; i < len; ++i) {
|
||||
out[i] = g_rand_int_range(rand, 1, 256);
|
||||
}
|
||||
|
||||
out[len] = 0;
|
||||
|
||||
g_rand_free(rand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check encrypt and decrypt in ECB mode
|
||||
*/
|
||||
static void
|
||||
test_ecb(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
char *de = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
|
||||
/* Encrypt */
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
|
||||
/* Decrypt */
|
||||
/* Linear */
|
||||
de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE);
|
||||
g_assert_cmpstr (de, ==, message);
|
||||
g_free(de);
|
||||
|
||||
/* Mixed */
|
||||
de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE);
|
||||
g_assert_cmpstr (de, ==, message);
|
||||
g_free(de);
|
||||
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check encrypt and decrypt in CBC mode
|
||||
*/
|
||||
static void
|
||||
test_cbc(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
char *de = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
|
||||
/* Encrypt */
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
|
||||
/* Decrypt */
|
||||
/* Linear */
|
||||
de = fish_decrypt_str(key, key_len, b64, FISH_CBC_MODE);
|
||||
g_assert_cmpstr (de, ==, message);
|
||||
g_free(de);
|
||||
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encoded string in Base64
|
||||
*/
|
||||
static void
|
||||
test_base64_len (void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
char message[1000];
|
||||
int message_end = sizeof (message) - 1;
|
||||
|
||||
random_string(message, message_end);
|
||||
|
||||
for (; message_end >= 0; --message_end) {
|
||||
message[message_end] = '\0'; /* Truncate instead of generating new strings */
|
||||
b64 = g_base64_encode((const unsigned char *) message, message_end);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , base64_len(message_end));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encoded string in BlowcryptBase64
|
||||
*/
|
||||
static void
|
||||
test_base64_fish_len (void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
int message_len = 0;
|
||||
char message[1000];
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
b64 = fish_base64_encode(message, message_len);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , base64_fish_len(message_len));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encrypted string in ECB mode
|
||||
*/
|
||||
static void
|
||||
test_base64_ecb_len(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , ecb_len(message_len));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encrypted string in CBC mode
|
||||
*/
|
||||
static void
|
||||
test_base64_cbc_len(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , cbc_len(message_len));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of length limit for a plaintext in each encryption mode
|
||||
*/
|
||||
static void
|
||||
test_max_text_command_len(void)
|
||||
{
|
||||
int max_encoded_len, plaintext_len;
|
||||
enum fish_mode mode;
|
||||
|
||||
for (max_encoded_len = 0; max_encoded_len < 10000; ++max_encoded_len) {
|
||||
for (mode = FISH_ECB_MODE; mode <= FISH_CBC_MODE; ++mode) {
|
||||
plaintext_len = max_text_command_len(max_encoded_len, mode);
|
||||
g_assert_cmpuint(encoded_len(plaintext_len, mode), <= , max_encoded_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of length limit for a plaintext in each encryption mode
|
||||
*/
|
||||
static void
|
||||
test_foreach_utf8_data_chunks(void)
|
||||
{
|
||||
GRand *rand = NULL;
|
||||
GString *chunks = NULL;
|
||||
int max_chunks_len, chunks_len;
|
||||
char ascii_message[1001];
|
||||
char *data_chunk = NULL;
|
||||
|
||||
rand = g_rand_new();
|
||||
max_chunks_len = g_rand_int_range(rand, 2, 301);
|
||||
random_string(ascii_message, 1000);
|
||||
|
||||
data_chunk = ascii_message;
|
||||
|
||||
chunks = g_string_new(NULL);
|
||||
|
||||
while (foreach_utf8_data_chunks(data_chunk, max_chunks_len, &chunks_len)) {
|
||||
g_string_append(chunks, g_strndup(data_chunk, chunks_len));
|
||||
/* Next chunk */
|
||||
data_chunk += chunks_len;
|
||||
}
|
||||
/* Check data loss */
|
||||
g_assert_cmpstr(chunks->str, == , ascii_message);
|
||||
|
||||
g_string_free(chunks, TRUE);
|
||||
g_rand_free (rand);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[]) {
|
||||
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
|
||||
g_test_add_func("/fishlim/ecb", test_ecb);
|
||||
g_test_add_func("/fishlim/cbc", test_cbc);
|
||||
g_test_add_func("/fishlim/base64_len", test_base64_len);
|
||||
g_test_add_func("/fishlim/base64_fish_len", test_base64_fish_len);
|
||||
g_test_add_func("/fishlim/base64_ecb_len", test_base64_ecb_len);
|
||||
g_test_add_func("/fishlim/base64_cbc_len", test_base64_cbc_len);
|
||||
g_test_add_func("/fishlim/max_text_command_len", test_max_text_command_len);
|
||||
g_test_add_func("/fishlim/foreach_utf8_data_chunks", test_foreach_utf8_data_chunks);
|
||||
|
||||
fish_init();
|
||||
int ret = g_test_run();
|
||||
fish_deinit();
|
||||
return ret;
|
||||
}
|
151
plugins/fishlim/utils.c
Normal file
151
plugins/fishlim/utils.c
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "utils.h"
|
||||
#include "fish.h"
|
||||
|
||||
/**
|
||||
* Calculate the length of Base64-encoded string
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long base64_len(size_t plaintext_len) {
|
||||
int length_unpadded = (4 * plaintext_len) / 3;
|
||||
/* Add padding */
|
||||
return length_unpadded % 4 != 0 ? length_unpadded + (4 - length_unpadded % 4) : length_unpadded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of BlowcryptBase64-encoded string
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long base64_fish_len(size_t plaintext_len) {
|
||||
int length_unpadded = (12 * plaintext_len) / 8;
|
||||
/* Add padding */
|
||||
return length_unpadded % 12 != 0 ? length_unpadded + (12 - length_unpadded % 12) : length_unpadded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of fish-encrypted string in CBC mode
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long cbc_len(size_t plaintext_len) {
|
||||
/*IV + DATA + Zero Padding */
|
||||
return base64_len(8 + (plaintext_len % 8 != 0 ? plaintext_len + 8 - (plaintext_len % 8) : plaintext_len));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of fish-encrypted string in ECB mode
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long ecb_len(size_t plaintext_len) {
|
||||
return base64_fish_len(plaintext_len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of encrypted string in 'mode' mode
|
||||
*
|
||||
* @param plaintext_len Length of plaintext
|
||||
* @param mode Encryption mode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode) {
|
||||
switch (mode) {
|
||||
|
||||
case FISH_CBC_MODE:
|
||||
return cbc_len(plaintext_len);
|
||||
break;
|
||||
|
||||
case FISH_ECB_MODE:
|
||||
return ecb_len(plaintext_len);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the maximum length of plaintext for a 'max_len' limit taking care the overload of encryption
|
||||
*
|
||||
* @param max_len Limit for plaintext
|
||||
* @param mode Encryption mode
|
||||
* @return Maximum allowed plaintext length
|
||||
*/
|
||||
int max_text_command_len(size_t max_len, enum fish_mode mode) {
|
||||
int len;
|
||||
|
||||
for (len = max_len; encoded_len(len, mode) > max_len; --len);
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over 'data' in chunks of 'max_chunk_len' taking care the UTF-8 characters
|
||||
*
|
||||
* @param data Data to iterate
|
||||
* @param max_chunk_len Size of biggest chunk
|
||||
* @param [out] chunk_len Current chunk length
|
||||
* @return Pointer to current chunk position or NULL if not have more chunks
|
||||
*/
|
||||
const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len) {
|
||||
int data_len, last_chunk_len = 0;
|
||||
|
||||
if (!*data) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Last chunk of data */
|
||||
data_len = strlen(data);
|
||||
if (data_len <= max_chunk_len) {
|
||||
*chunk_len = data_len;
|
||||
return data;
|
||||
}
|
||||
|
||||
*chunk_len = 0;
|
||||
const char *utf8_character = data;
|
||||
|
||||
/* Not valid UTF-8, but maybe valid text, just split into max length */
|
||||
if (!g_utf8_validate(data, -1, NULL)) {
|
||||
*chunk_len = max_chunk_len;
|
||||
return utf8_character;
|
||||
}
|
||||
|
||||
while (*utf8_character && *chunk_len <= max_chunk_len) {
|
||||
last_chunk_len = *chunk_len;
|
||||
*chunk_len = (g_utf8_next_char(utf8_character) - data) * sizeof(*utf8_character);
|
||||
utf8_character = g_utf8_next_char(utf8_character);
|
||||
}
|
||||
|
||||
/* We need the previous length before overflow the limit */
|
||||
*chunk_len = last_chunk_len;
|
||||
|
||||
return utf8_character;
|
||||
}
|
39
plugins/fishlim/utils.h
Normal file
39
plugins/fishlim/utils.h
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#ifndef PLUGIN_HEXCHAT_FISHLIM_UTILS_H
|
||||
#define PLUGIN_HEXCHAT_FISHLIM_UTILS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "fish.h"
|
||||
|
||||
unsigned long base64_len(size_t plaintext_len);
|
||||
unsigned long base64_fish_len(size_t plaintext_len);
|
||||
unsigned long cbc_len(size_t plaintext_len);
|
||||
unsigned long ecb_len(size_t plaintext_len);
|
||||
unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode);
|
||||
int max_text_command_len(size_t max_len, enum fish_mode mode);
|
||||
const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len);
|
||||
|
||||
#endif
|
@ -35,6 +35,8 @@
|
||||
|
||||
#include <hexchat-plugin.h>
|
||||
|
||||
#define WORD_ARRAY_LEN 32
|
||||
|
||||
static char plugin_name[] = "Lua";
|
||||
static char plugin_description[] = "Lua scripting interface";
|
||||
static char plugin_version[16] = "1.3";
|
||||
@ -275,13 +277,13 @@ static int api_command_closure(char *word[], char *word_eol[], void *udata)
|
||||
base = lua_gettop(L);
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
}
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word_eol[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
@ -462,13 +464,13 @@ static int api_server_closure(char *word[], char *word_eol[], void *udata)
|
||||
base = lua_gettop(L);
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
}
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word_eol[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
@ -521,13 +523,13 @@ static int api_server_attrs_closure(char *word[], char *word_eol[], hexchat_even
|
||||
base = lua_gettop(L);
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
}
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word_eol[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
@ -955,16 +957,21 @@ static int api_hexchat_pluginprefs_meta_pairs(lua_State *L)
|
||||
hexchat_plugin *h;
|
||||
|
||||
if(!script->name)
|
||||
|
||||
return luaL_error(L, "cannot use hexchat.pluginprefs before registering with hexchat.register");
|
||||
|
||||
dest = lua_newuserdata(L, 4096);
|
||||
|
||||
h = script->handle;
|
||||
if(!hexchat_pluginpref_list(h, dest))
|
||||
strcpy(dest, "");
|
||||
lua_pushlightuserdata(L, dest);
|
||||
lua_pushlightuserdata(L, dest);
|
||||
lua_pushcclosure(L, api_hexchat_pluginprefs_meta_pairs_closure, 2);
|
||||
return 1;
|
||||
lua_insert(L, -2); // Return the userdata (second return value from pairs),
|
||||
// even though it's not used by the closure (first return
|
||||
// value from pairs), so that Lua knows not to GC it.
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int api_attrs_meta_index(lua_State *L)
|
||||
@ -1187,11 +1194,11 @@ static void patch_clibs(lua_State *L)
|
||||
if(lua_type(L, -2) == LUA_TLIGHTUSERDATA && lua_type(L, -1) == LUA_TTABLE)
|
||||
{
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_CLIBS");
|
||||
lua_pop(L, 1);
|
||||
break;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static GPtrArray *scripts;
|
||||
@ -1762,4 +1769,3 @@ G_MODULE_EXPORT int hexchat_plugin_deinit(hexchat_plugin *plugin_handle)
|
||||
g_clear_pointer(&expand_buffer, g_free);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -26,7 +26,7 @@ if get_option('with-lua') != 'false'
|
||||
subdir('lua')
|
||||
endif
|
||||
|
||||
if get_option('with-perl')
|
||||
if get_option('with-perl') != 'false'
|
||||
subdir('perl')
|
||||
endif
|
||||
|
||||
|
@ -51,7 +51,7 @@ sub get_context;
|
||||
sub HexChat::Internal::context_info;
|
||||
sub HexChat::Internal::print;
|
||||
|
||||
#keep compability with Xchat scripts
|
||||
#keep compatibility with Xchat scripts
|
||||
sub EAT_XCHAT ();
|
||||
BEGIN {
|
||||
*Xchat:: = *HexChat::;
|
||||
|
@ -13,19 +13,24 @@ hexchat_perl_module = custom_target('hexchat-perl-header',
|
||||
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
|
||||
)
|
||||
|
||||
irc_perl_module = custom_target('irc-perl-header',
|
||||
input: 'lib/IRC.pm',
|
||||
output: 'irc.pm.h',
|
||||
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
|
||||
)
|
||||
perl_cflags = []
|
||||
irc_perl_module = []
|
||||
|
||||
perl = find_program('perl')
|
||||
if get_option('with-perl-legacy-api')
|
||||
irc_perl_module = custom_target('irc-perl-header',
|
||||
input: 'lib/IRC.pm',
|
||||
output: 'irc.pm.h',
|
||||
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
|
||||
)
|
||||
perl_cflags += '-DOLD_PERL'
|
||||
endif
|
||||
|
||||
perl = find_program(get_option('with-perl'))
|
||||
|
||||
ret = run_command([perl, '-MExtUtils::Embed', '-e', 'ccopts'])
|
||||
if ret.returncode() != 0
|
||||
error('perl: Failed to get cflags')
|
||||
endif
|
||||
perl_cflags = []
|
||||
foreach flag : ret.stdout().strip().split(' ')
|
||||
if flag.startswith('-I') or flag.startswith('-D')
|
||||
perl_cflags += flag
|
||||
@ -71,13 +76,17 @@ int main(void) {
|
||||
error('found perl not suitable for plugin')
|
||||
endif
|
||||
|
||||
perl_dep = declare_dependency(
|
||||
compile_args: perl_cflags,
|
||||
link_args: perl_ldflags
|
||||
)
|
||||
|
||||
shared_module('perl',
|
||||
sources: ['perl.c', hexchat_perl_module, irc_perl_module],
|
||||
dependencies: [libgio_dep, hexchat_plugin_dep],
|
||||
c_args: perl_cflags,
|
||||
link_args: perl_ldflags,
|
||||
dependencies: [libgio_dep, hexchat_plugin_dep, perl_dep],
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
install_rpath: perl_rpath,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'perl.def',
|
||||
)
|
||||
|
@ -283,6 +283,8 @@ list_item_to_sv ( hexchat_list *list, const char *const *fields )
|
||||
return sv_2mortal (newRV_noinc ((SV *) hash));
|
||||
}
|
||||
|
||||
#define WORD_ARRAY_LEN 32
|
||||
|
||||
static AV *
|
||||
array2av (char *array[])
|
||||
{
|
||||
@ -293,7 +295,7 @@ array2av (char *array[])
|
||||
|
||||
for (
|
||||
count = 1;
|
||||
count < 32 && array[count] != NULL && array[count][0] != 0;
|
||||
count < WORD_ARRAY_LEN && array[count] != NULL && array[count][0] != 0;
|
||||
count++
|
||||
) {
|
||||
temp = newSVpv (array[count], 0);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
386
plugins/python/_hexchat.py
Normal file
386
plugins/python/_hexchat.py
Normal file
@ -0,0 +1,386 @@
|
||||
import inspect
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
from _hexchat_embedded import ffi, lib
|
||||
|
||||
__all__ = [
|
||||
'EAT_ALL', 'EAT_HEXCHAT', 'EAT_NONE', 'EAT_PLUGIN', 'EAT_XCHAT',
|
||||
'PRI_HIGH', 'PRI_HIGHEST', 'PRI_LOW', 'PRI_LOWEST', 'PRI_NORM',
|
||||
'__doc__', '__version__', 'command', 'del_pluginpref', 'emit_print',
|
||||
'find_context', 'get_context', 'get_info',
|
||||
'get_list', 'get_lists', 'get_pluginpref', 'get_prefs', 'hook_command',
|
||||
'hook_print', 'hook_print_attrs', 'hook_server', 'hook_server_attrs',
|
||||
'hook_timer', 'hook_unload', 'list_pluginpref', 'nickcmp', 'prnt',
|
||||
'set_pluginpref', 'strip', 'unhook',
|
||||
]
|
||||
|
||||
__doc__ = 'HexChat Scripting Interface'
|
||||
__version__ = (2, 0)
|
||||
__license__ = 'GPL-2.0+'
|
||||
|
||||
EAT_NONE = 0
|
||||
EAT_HEXCHAT = 1
|
||||
EAT_XCHAT = EAT_HEXCHAT
|
||||
EAT_PLUGIN = 2
|
||||
EAT_ALL = EAT_HEXCHAT | EAT_PLUGIN
|
||||
|
||||
PRI_LOWEST = -128
|
||||
PRI_LOW = -64
|
||||
PRI_NORM = 0
|
||||
PRI_HIGH = 64
|
||||
PRI_HIGHEST = 127
|
||||
|
||||
|
||||
# We need each module to be able to reference their parent plugin
|
||||
# which is a bit tricky since they all share the exact same module.
|
||||
# Simply navigating up to what module called it seems to actually
|
||||
# be a fairly reliable and simple method of doing so if ugly.
|
||||
def __get_current_plugin():
|
||||
frame = inspect.stack()[1][0]
|
||||
while '__plugin' not in frame.f_globals:
|
||||
frame = frame.f_back
|
||||
assert frame is not None
|
||||
|
||||
return frame.f_globals['__plugin']
|
||||
|
||||
|
||||
# Keeping API compat
|
||||
if sys.version_info[0] == 2:
|
||||
def __decode(string):
|
||||
return string
|
||||
|
||||
else:
|
||||
def __decode(string):
|
||||
return string.decode()
|
||||
|
||||
|
||||
# ------------ API ------------
|
||||
def prnt(string):
|
||||
lib.hexchat_print(lib.ph, string.encode())
|
||||
|
||||
|
||||
def emit_print(event_name, *args, **kwargs):
|
||||
time = kwargs.pop('time', 0) # For py2 compat
|
||||
cargs = []
|
||||
for i in range(4):
|
||||
arg = args[i].encode() if len(args) > i else b''
|
||||
cstring = ffi.new('char[]', arg)
|
||||
cargs.append(cstring)
|
||||
|
||||
if time == 0:
|
||||
return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs)
|
||||
|
||||
attrs = lib.hexchat_event_attrs_create(lib.ph)
|
||||
attrs.server_time_utc = time
|
||||
ret = lib.hexchat_emit_print_attrs(lib.ph, attrs, event_name.encode(), *cargs)
|
||||
lib.hexchat_event_attrs_free(lib.ph, attrs)
|
||||
return ret
|
||||
|
||||
|
||||
# TODO: this shadows itself. command should be changed to cmd
|
||||
def command(command):
|
||||
lib.hexchat_command(lib.ph, command.encode())
|
||||
|
||||
|
||||
def nickcmp(string1, string2):
|
||||
return lib.hexchat_nickcmp(lib.ph, string1.encode(), string2.encode())
|
||||
|
||||
|
||||
def strip(text, length=-1, flags=3):
|
||||
stripped = lib.hexchat_strip(lib.ph, text.encode(), length, flags)
|
||||
ret = __decode(ffi.string(stripped))
|
||||
lib.hexchat_free(lib.ph, stripped)
|
||||
return ret
|
||||
|
||||
|
||||
def get_info(name):
|
||||
ret = lib.hexchat_get_info(lib.ph, name.encode())
|
||||
if ret == ffi.NULL:
|
||||
return None
|
||||
if name in ('gtkwin_ptr', 'win_ptr'):
|
||||
# Surely there is a less dumb way?
|
||||
ptr = repr(ret).rsplit(' ', 1)[1][:-1]
|
||||
return ptr
|
||||
|
||||
return __decode(ffi.string(ret))
|
||||
|
||||
|
||||
def get_prefs(name):
|
||||
string_out = ffi.new('char**')
|
||||
int_out = ffi.new('int*')
|
||||
_type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out)
|
||||
if _type == 0:
|
||||
return None
|
||||
|
||||
if _type == 1:
|
||||
return __decode(ffi.string(string_out[0]))
|
||||
|
||||
if _type in (2, 3): # XXX: 3 should be a bool, but keeps API
|
||||
return int_out[0]
|
||||
|
||||
raise AssertionError('Out of bounds pref storage')
|
||||
|
||||
|
||||
def __cstrarray_to_list(arr):
|
||||
i = 0
|
||||
ret = []
|
||||
while arr[i] != ffi.NULL:
|
||||
ret.append(ffi.string(arr[i]))
|
||||
i += 1
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
__FIELD_CACHE = {}
|
||||
|
||||
|
||||
def __get_fields(name):
|
||||
return __FIELD_CACHE.setdefault(name, __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name)))
|
||||
|
||||
|
||||
__FIELD_PROPERTY_CACHE = {}
|
||||
|
||||
|
||||
def __cached_decoded_str(string):
|
||||
return __FIELD_PROPERTY_CACHE.setdefault(string, __decode(string))
|
||||
|
||||
|
||||
def get_lists():
|
||||
return [__cached_decoded_str(field) for field in __get_fields(b'lists')]
|
||||
|
||||
|
||||
class ListItem:
|
||||
def __init__(self, name):
|
||||
self._listname = name
|
||||
|
||||
def __repr__(self):
|
||||
return '<{} list item at {}>'.format(self._listname, id(self))
|
||||
|
||||
|
||||
# done this way for speed
|
||||
if sys.version_info[0] == 2:
|
||||
def get_getter(name):
|
||||
return ord(name[0])
|
||||
|
||||
else:
|
||||
def get_getter(name):
|
||||
return name[0]
|
||||
|
||||
|
||||
def get_list(name):
|
||||
# XXX: This function is extremely inefficient and could be interators and
|
||||
# lazily loaded properties, but for API compat we stay slow
|
||||
orig_name = name
|
||||
name = name.encode()
|
||||
|
||||
if name not in __get_fields(b'lists'):
|
||||
raise KeyError('list not available')
|
||||
|
||||
list_ = lib.hexchat_list_get(lib.ph, name)
|
||||
if list_ == ffi.NULL:
|
||||
return None
|
||||
|
||||
ret = []
|
||||
fields = __get_fields(name)
|
||||
|
||||
def string_getter(field):
|
||||
string = lib.hexchat_list_str(lib.ph, list_, field)
|
||||
if string != ffi.NULL:
|
||||
return __decode(ffi.string(string))
|
||||
|
||||
return ''
|
||||
|
||||
def ptr_getter(field):
|
||||
if field == b'context':
|
||||
ptr = lib.hexchat_list_str(lib.ph, list_, field)
|
||||
ctx = ffi.cast('hexchat_context*', ptr)
|
||||
return Context(ctx)
|
||||
|
||||
return None
|
||||
|
||||
getters = {
|
||||
ord('s'): string_getter,
|
||||
ord('i'): lambda field: lib.hexchat_list_int(lib.ph, list_, field),
|
||||
ord('t'): lambda field: lib.hexchat_list_time(lib.ph, list_, field),
|
||||
ord('p'): ptr_getter,
|
||||
}
|
||||
|
||||
while lib.hexchat_list_next(lib.ph, list_) == 1:
|
||||
item = ListItem(orig_name)
|
||||
for _field in fields:
|
||||
getter = getters.get(get_getter(_field))
|
||||
if getter is not None:
|
||||
field_name = _field[1:]
|
||||
setattr(item, __cached_decoded_str(field_name), getter(field_name))
|
||||
|
||||
ret.append(item)
|
||||
|
||||
lib.hexchat_list_free(lib.ph, list_)
|
||||
return ret
|
||||
|
||||
|
||||
# TODO: 'command' here shadows command above, and should be renamed to cmd
|
||||
def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook,
|
||||
help.encode() if help is not None else ffi.NULL, hook.handle)
|
||||
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_print(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_server(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_timer(timeout, callback, userdata=None):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_timer(lib.ph, timeout, lib._on_timer_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_unload(callback, userdata=None):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata, is_unload=True)
|
||||
return id(hook)
|
||||
|
||||
|
||||
def unhook(handle):
|
||||
plugin = __get_current_plugin()
|
||||
return plugin.remove_hook(handle)
|
||||
|
||||
|
||||
def set_pluginpref(name, value):
|
||||
if isinstance(value, str):
|
||||
return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode()))
|
||||
|
||||
if isinstance(value, int):
|
||||
return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value))
|
||||
|
||||
# XXX: This should probably raise but this keeps API
|
||||
return False
|
||||
|
||||
|
||||
def get_pluginpref(name):
|
||||
name = name.encode()
|
||||
string_out = ffi.new('char[512]')
|
||||
if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) != 1:
|
||||
return None
|
||||
|
||||
string = ffi.string(string_out)
|
||||
# This API stores everything as a string so we have to figure out what
|
||||
# its actual type was supposed to be.
|
||||
if len(string) > 12: # Can't be a number
|
||||
return __decode(string)
|
||||
|
||||
number = lib.hexchat_pluginpref_get_int(lib.ph, name)
|
||||
if number == -1 and string != b'-1':
|
||||
return __decode(string)
|
||||
|
||||
return number
|
||||
|
||||
|
||||
def del_pluginpref(name):
|
||||
return bool(lib.hexchat_pluginpref_delete(lib.ph, name.encode()))
|
||||
|
||||
|
||||
def list_pluginpref():
|
||||
prefs_str = ffi.new('char[4096]')
|
||||
if lib.hexchat_pluginpref_list(lib.ph, prefs_str) == 1:
|
||||
return __decode(ffi.string(prefs_str)).split(',')
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, ctx):
|
||||
self._ctx = ctx
|
||||
|
||||
def __eq__(self, value):
|
||||
if not isinstance(value, Context):
|
||||
return False
|
||||
|
||||
return self._ctx == value._ctx
|
||||
|
||||
@contextmanager
|
||||
def __change_context(self):
|
||||
old_ctx = lib.hexchat_get_context(lib.ph)
|
||||
if not self.set():
|
||||
# XXX: Behavior change, previously used wrong context
|
||||
lib.hexchat_print(lib.ph, b'Context object refers to closed context, ignoring call')
|
||||
return
|
||||
|
||||
yield
|
||||
lib.hexchat_set_context(lib.ph, old_ctx)
|
||||
|
||||
def set(self):
|
||||
# XXX: API addition, C plugin silently ignored failure
|
||||
return bool(lib.hexchat_set_context(lib.ph, self._ctx))
|
||||
|
||||
def prnt(self, string):
|
||||
with self.__change_context():
|
||||
prnt(string)
|
||||
|
||||
def emit_print(self, event_name, *args, **kwargs):
|
||||
time = kwargs.pop('time', 0) # For py2 compat
|
||||
with self.__change_context():
|
||||
return emit_print(event_name, *args, time=time)
|
||||
|
||||
def command(self, string):
|
||||
with self.__change_context():
|
||||
command(string)
|
||||
|
||||
def get_info(self, name):
|
||||
with self.__change_context():
|
||||
return get_info(name)
|
||||
|
||||
def get_list(self, name):
|
||||
with self.__change_context():
|
||||
return get_list(name)
|
||||
|
||||
|
||||
def get_context():
|
||||
ctx = lib.hexchat_get_context(lib.ph)
|
||||
return Context(ctx)
|
||||
|
||||
|
||||
def find_context(server=None, channel=None):
|
||||
server = server.encode() if server is not None else ffi.NULL
|
||||
channel = channel.encode() if channel is not None else ffi.NULL
|
||||
ctx = lib.hexchat_find_context(lib.ph, server, channel)
|
||||
if ctx == ffi.NULL:
|
||||
return None
|
||||
|
||||
return Context(ctx)
|
89
plugins/python/generate_plugin.py
Executable file
89
plugins/python/generate_plugin.py
Executable file
@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import cffi
|
||||
|
||||
builder = cffi.FFI()
|
||||
|
||||
# hexchat-plugin.h
|
||||
with open(sys.argv[1]) as f:
|
||||
output = []
|
||||
eat_until_endif = 0
|
||||
# This is very specific to hexchat-plugin.h, it is not a cpp
|
||||
for line in f:
|
||||
if line.startswith('#define'):
|
||||
continue
|
||||
elif line.endswith('HEXCHAT_PLUGIN_H\n'):
|
||||
continue
|
||||
elif 'time.h' in line:
|
||||
output.append('typedef int... time_t;')
|
||||
elif line.startswith('#if'):
|
||||
eat_until_endif += 1
|
||||
elif line.startswith('#endif'):
|
||||
eat_until_endif -= 1
|
||||
elif eat_until_endif and '_hexchat_context' not in line:
|
||||
continue
|
||||
else:
|
||||
output.append(line)
|
||||
builder.cdef(''.join(output))
|
||||
|
||||
builder.embedding_api('''
|
||||
extern "Python" int _on_py_command(char **, char **, void *);
|
||||
extern "Python" int _on_load_command(char **, char **, void *);
|
||||
extern "Python" int _on_unload_command(char **, char **, void *);
|
||||
extern "Python" int _on_reload_command(char **, char **, void *);
|
||||
extern "Python" int _on_say_command(char **, char **, void *);
|
||||
|
||||
extern "Python" int _on_command_hook(char **, char **, void *);
|
||||
extern "Python" int _on_print_hook(char **, void *);
|
||||
extern "Python" int _on_print_attrs_hook(char **, hexchat_event_attrs *, void *);
|
||||
extern "Python" int _on_server_hook(char **, char **, void *);
|
||||
extern "Python" int _on_server_attrs_hook(char **, char **, hexchat_event_attrs *, void *);
|
||||
extern "Python" int _on_timer_hook(void *);
|
||||
|
||||
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||
extern "Python" int _on_plugin_deinit(void);
|
||||
|
||||
static hexchat_plugin *ph;
|
||||
''')
|
||||
|
||||
builder.set_source('_hexchat_embedded', '''
|
||||
/* Python's header defines these.. */
|
||||
#undef HAVE_MEMRCHR
|
||||
#undef HAVE_STRINGS_H
|
||||
|
||||
#include "config.h"
|
||||
#include "hexchat-plugin.h"
|
||||
|
||||
static hexchat_plugin *ph;
|
||||
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||
CFFI_DLLEXPORT int _on_plugin_deinit(void);
|
||||
|
||||
int hexchat_plugin_init(hexchat_plugin *plugin_handle,
|
||||
char **name_out, char **description_out,
|
||||
char **version_out, char *arg)
|
||||
{
|
||||
if (ph != NULL)
|
||||
{
|
||||
puts ("Python plugin already loaded\\n");
|
||||
return 0; /* Prevent loading twice */
|
||||
}
|
||||
|
||||
ph = plugin_handle;
|
||||
return _on_plugin_init(name_out, description_out, version_out, arg, HEXCHATLIBDIR);
|
||||
}
|
||||
|
||||
int hexchat_plugin_deinit(void)
|
||||
{
|
||||
int ret = _on_plugin_deinit();
|
||||
ph = NULL;
|
||||
return ret;
|
||||
}
|
||||
''')
|
||||
|
||||
# python.py
|
||||
with open(sys.argv[2]) as f:
|
||||
builder.embedding_init_code(f.read())
|
||||
|
||||
# python.c
|
||||
builder.emit_c_code(sys.argv[3])
|
1
plugins/python/hexchat.py
Normal file
1
plugins/python/hexchat.py
Normal file
@ -0,0 +1 @@
|
||||
from _hexchat import *
|
@ -1,13 +1,32 @@
|
||||
python_opt = get_option('with-python')
|
||||
if python_opt.startswith('python3')
|
||||
python_dep = dependency(python_opt, version: '>= 3.3')
|
||||
# Python 3.8 introduced a new -embed variant
|
||||
if not python_opt.endswith('-embed')
|
||||
python_dep = dependency(python_opt + '-embed', version: '>= 3.3', required: false)
|
||||
if not python_dep.found()
|
||||
python_dep = dependency(python_opt, version: '>= 3.3')
|
||||
endif
|
||||
else
|
||||
python_dep = dependency(python_opt, version: '>= 3.3')
|
||||
endif
|
||||
else
|
||||
python_dep = dependency(python_opt, version: '>= 2.7')
|
||||
endif
|
||||
|
||||
shared_module('python', 'python.c',
|
||||
dependencies: [libgio_dep, hexchat_plugin_dep, python_dep],
|
||||
python3_source = custom_target('python-bindings',
|
||||
input: ['../../src/common/hexchat-plugin.h', 'python.py'],
|
||||
output: 'python.c',
|
||||
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
|
||||
)
|
||||
|
||||
install_data(['_hexchat.py', 'hexchat.py', 'xchat.py'],
|
||||
install_dir: join_paths(get_option('libdir'), 'hexchat/python')
|
||||
)
|
||||
|
||||
shared_module('python', python3_source,
|
||||
dependencies: [hexchat_plugin_dep, python_dep],
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'python.def'
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
EXPORTS
|
||||
hexchat_plugin_init
|
||||
hexchat_plugin_deinit
|
||||
hexchat_plugin_get_info
|
||||
|
554
plugins/python/python.py
Normal file
554
plugins/python/python.py
Normal file
@ -0,0 +1,554 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import pydoc
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
import weakref
|
||||
from contextlib import contextmanager
|
||||
|
||||
from _hexchat_embedded import ffi, lib
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
from io import BytesIO as HelpEater
|
||||
else:
|
||||
from io import StringIO as HelpEater
|
||||
|
||||
if not hasattr(sys, 'argv'):
|
||||
sys.argv = ['<hexchat>']
|
||||
|
||||
VERSION = b'2.0' # Sync with hexchat.__version__
|
||||
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)
|
||||
|
||||
# TODO: Constants should be screaming snake case
|
||||
hexchat = None
|
||||
local_interp = None
|
||||
hexchat_stdout = None
|
||||
plugins = set()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def redirected_stdout():
|
||||
sys.stdout = sys.__stdout__
|
||||
sys.stderr = sys.__stderr__
|
||||
yield
|
||||
sys.stdout = hexchat_stdout
|
||||
sys.stderr = hexchat_stdout
|
||||
|
||||
|
||||
if os.getenv('HEXCHAT_LOG_PYTHON'):
|
||||
def log(*args):
|
||||
with redirected_stdout():
|
||||
print(*args)
|
||||
|
||||
else:
|
||||
def log(*args):
|
||||
pass
|
||||
|
||||
|
||||
class Stdout:
|
||||
def __init__(self):
|
||||
self.buffer = bytearray()
|
||||
|
||||
def write(self, string):
|
||||
string = string.encode()
|
||||
idx = string.rfind(b'\n')
|
||||
if idx != -1:
|
||||
self.buffer += string[:idx]
|
||||
lib.hexchat_print(lib.ph, bytes(self.buffer))
|
||||
self.buffer = bytearray(string[idx + 1:])
|
||||
else:
|
||||
self.buffer += string
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
|
||||
class Attribute:
|
||||
def __init__(self):
|
||||
self.time = 0
|
||||
|
||||
def __repr__(self):
|
||||
return '<Attribute object at {}>'.format(id(self))
|
||||
|
||||
|
||||
class Hook:
|
||||
def __init__(self, plugin, callback, userdata, is_unload):
|
||||
self.is_unload = is_unload
|
||||
self.plugin = weakref.proxy(plugin)
|
||||
self.callback = callback
|
||||
self.userdata = userdata
|
||||
self.hexchat_hook = None
|
||||
self.handle = ffi.new_handle(weakref.proxy(self))
|
||||
|
||||
def __del__(self):
|
||||
log('Removing hook', id(self))
|
||||
if self.is_unload is False:
|
||||
assert self.hexchat_hook is not None
|
||||
lib.hexchat_unhook(lib.ph, self.hexchat_hook)
|
||||
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
def compile_file(data, filename):
|
||||
return compile(data, filename, 'exec', dont_inherit=True)
|
||||
|
||||
|
||||
def compile_line(string):
|
||||
try:
|
||||
return compile(string, '<string>', 'eval', dont_inherit=True)
|
||||
|
||||
except SyntaxError:
|
||||
# For some reason `print` is invalid for eval
|
||||
# This will hide any return value though
|
||||
return compile(string, '<string>', 'exec', dont_inherit=True)
|
||||
else:
|
||||
def compile_file(data, filename):
|
||||
return compile(data, filename, 'exec', optimize=2, dont_inherit=True)
|
||||
|
||||
|
||||
def compile_line(string):
|
||||
# newline appended to solve unexpected EOF issues
|
||||
return compile(string + '\n', '<string>', 'single', optimize=2, dont_inherit=True)
|
||||
|
||||
|
||||
class Plugin:
|
||||
def __init__(self):
|
||||
self.ph = None
|
||||
self.name = ''
|
||||
self.filename = ''
|
||||
self.version = ''
|
||||
self.description = ''
|
||||
self.hooks = set()
|
||||
self.globals = {
|
||||
'__plugin': weakref.proxy(self),
|
||||
'__name__': '__main__',
|
||||
}
|
||||
|
||||
def add_hook(self, callback, userdata, is_unload=False):
|
||||
hook = Hook(self, callback, userdata, is_unload=is_unload)
|
||||
self.hooks.add(hook)
|
||||
return hook
|
||||
|
||||
def remove_hook(self, hook):
|
||||
for h in self.hooks:
|
||||
if id(h) == hook:
|
||||
ud = h.userdata
|
||||
self.hooks.remove(h)
|
||||
return ud
|
||||
|
||||
log('Hook not found')
|
||||
return None
|
||||
|
||||
def loadfile(self, filename):
|
||||
try:
|
||||
self.filename = filename
|
||||
with open(filename, 'rb') as f:
|
||||
data = f.read().decode('utf-8')
|
||||
compiled = compile_file(data, filename)
|
||||
exec(compiled, self.globals)
|
||||
|
||||
try:
|
||||
self.name = self.globals['__module_name__']
|
||||
|
||||
except KeyError:
|
||||
lib.hexchat_print(lib.ph, b'Failed to load module: __module_name__ must be set')
|
||||
|
||||
return False
|
||||
|
||||
self.version = self.globals.get('__module_version__', '')
|
||||
self.description = self.globals.get('__module_description__', '')
|
||||
self.ph = lib.hexchat_plugingui_add(lib.ph, filename.encode(), self.name.encode(),
|
||||
self.description.encode(), self.version.encode(), ffi.NULL)
|
||||
|
||||
except Exception as e:
|
||||
lib.hexchat_print(lib.ph, 'Failed to load module: {}'.format(e).encode())
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __del__(self):
|
||||
log('unloading', self.filename)
|
||||
for hook in self.hooks:
|
||||
if hook.is_unload is True:
|
||||
try:
|
||||
hook.callback(hook.userdata)
|
||||
|
||||
except Exception as e:
|
||||
log('Failed to run hook:', e)
|
||||
traceback.print_exc()
|
||||
|
||||
del self.hooks
|
||||
if self.ph is not None:
|
||||
lib.hexchat_plugingui_remove(lib.ph, self.ph)
|
||||
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
def __decode(string):
|
||||
return string
|
||||
|
||||
else:
|
||||
def __decode(string):
|
||||
return string.decode()
|
||||
|
||||
|
||||
# There can be empty entries between non-empty ones so find the actual last value
|
||||
def wordlist_len(words):
|
||||
for i in range(31, 0, -1):
|
||||
if ffi.string(words[i]):
|
||||
return i
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def create_wordlist(words):
|
||||
size = wordlist_len(words)
|
||||
return [__decode(ffi.string(words[i])) for i in range(1, size + 1)]
|
||||
|
||||
|
||||
# This function only exists for compat reasons with the C plugin
|
||||
# It turns the word list from print hooks into a word_eol list
|
||||
# This makes no sense to do...
|
||||
def create_wordeollist(words):
|
||||
words = reversed(words)
|
||||
accum = None
|
||||
ret = []
|
||||
for word in words:
|
||||
if accum is None:
|
||||
accum = word
|
||||
|
||||
elif word:
|
||||
last = accum
|
||||
accum = ' '.join((word, last))
|
||||
|
||||
ret.insert(0, accum)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def to_cb_ret(value):
|
||||
if value is None:
|
||||
return 0
|
||||
|
||||
return int(value)
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_command_hook(word, word_eol, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordlist(word_eol)
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_print_hook(word, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordeollist(word)
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_print_attrs_hook(word, attrs, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordeollist(word)
|
||||
attr = Attribute()
|
||||
attr.time = attrs.server_time_utc
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_server_hook(word, word_eol, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordlist(word_eol)
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_server_attrs_hook(word, word_eol, attrs, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordlist(word_eol)
|
||||
attr = Attribute()
|
||||
attr.time = attrs.server_time_utc
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_timer_hook(userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
if hook.callback(hook.userdata) == True:
|
||||
return 1
|
||||
|
||||
hook.is_unload = True # Don't unhook
|
||||
for h in hook.plugin.hooks:
|
||||
if h == hook:
|
||||
hook.plugin.hooks.remove(h)
|
||||
break
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern(error=3)
|
||||
def _on_say_command(word, word_eol, userdata):
|
||||
channel = ffi.string(lib.hexchat_get_info(lib.ph, b'channel'))
|
||||
if channel == b'>>python<<':
|
||||
python = ffi.string(word_eol[1])
|
||||
lib.hexchat_print(lib.ph, b'>>> ' + python)
|
||||
exec_in_interp(__decode(python))
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def load_filename(filename):
|
||||
filename = os.path.expanduser(filename)
|
||||
if not os.path.isabs(filename):
|
||||
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
|
||||
|
||||
filename = os.path.join(configdir, 'addons', filename)
|
||||
|
||||
if filename and not any(plugin.filename == filename for plugin in plugins):
|
||||
plugin = Plugin()
|
||||
if plugin.loadfile(filename):
|
||||
plugins.add(plugin)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def unload_name(name):
|
||||
if name:
|
||||
for plugin in plugins:
|
||||
if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)):
|
||||
plugins.remove(plugin)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def reload_name(name):
|
||||
if name:
|
||||
for plugin in plugins:
|
||||
if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)):
|
||||
filename = plugin.filename
|
||||
plugins.remove(plugin)
|
||||
return load_filename(filename)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def change_cwd(path):
|
||||
old_cwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
yield
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
def autoload():
|
||||
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
|
||||
addondir = os.path.join(configdir, 'addons')
|
||||
try:
|
||||
with change_cwd(addondir): # Maintaining old behavior
|
||||
for f in os.listdir(addondir):
|
||||
if f.endswith('.py'):
|
||||
log('Autoloading', f)
|
||||
# TODO: Set cwd
|
||||
load_filename(os.path.join(addondir, f))
|
||||
|
||||
except FileNotFoundError as e:
|
||||
log('Autoload failed', e)
|
||||
|
||||
|
||||
def list_plugins():
|
||||
if not plugins:
|
||||
lib.hexchat_print(lib.ph, b'No python modules loaded')
|
||||
return
|
||||
|
||||
tbl_headers = [b'Name', b'Version', b'Filename', b'Description']
|
||||
tbl = [
|
||||
tbl_headers,
|
||||
[(b'-' * len(s)) for s in tbl_headers]
|
||||
]
|
||||
|
||||
for plugin in plugins:
|
||||
basename = os.path.basename(plugin.filename).encode()
|
||||
name = plugin.name.encode()
|
||||
version = plugin.version.encode() if plugin.version else b'<none>'
|
||||
description = plugin.description.encode() if plugin.description else b'<none>'
|
||||
tbl.append((name, version, basename, description))
|
||||
|
||||
column_sizes = [
|
||||
max(len(item) for item in column)
|
||||
for column in zip(*tbl)
|
||||
]
|
||||
|
||||
for row in tbl:
|
||||
lib.hexchat_print(lib.ph, b' '.join(item.ljust(column_sizes[i])
|
||||
for i, item in enumerate(row)))
|
||||
lib.hexchat_print(lib.ph, b'')
|
||||
|
||||
|
||||
def exec_in_interp(python):
|
||||
global local_interp
|
||||
|
||||
if not python:
|
||||
return
|
||||
|
||||
if local_interp is None:
|
||||
local_interp = Plugin()
|
||||
local_interp.locals = {}
|
||||
local_interp.globals['hexchat'] = hexchat
|
||||
|
||||
code = compile_line(python)
|
||||
try:
|
||||
ret = eval(code, local_interp.globals, local_interp.locals)
|
||||
if ret is not None:
|
||||
lib.hexchat_print(lib.ph, '{}'.format(ret).encode())
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc(file=hexchat_stdout)
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_load_command(word, word_eol, userdata):
|
||||
filename = ffi.string(word[2])
|
||||
if filename.endswith(b'.py'):
|
||||
load_filename(__decode(filename))
|
||||
return 3
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_unload_command(word, word_eol, userdata):
|
||||
filename = ffi.string(word[2])
|
||||
if filename.endswith(b'.py'):
|
||||
unload_name(__decode(filename))
|
||||
return 3
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_reload_command(word, word_eol, userdata):
|
||||
filename = ffi.string(word[2])
|
||||
if filename.endswith(b'.py'):
|
||||
reload_name(__decode(filename))
|
||||
return 3
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern(error=3)
|
||||
def _on_py_command(word, word_eol, userdata):
|
||||
subcmd = __decode(ffi.string(word[2])).lower()
|
||||
|
||||
if subcmd == 'exec':
|
||||
python = __decode(ffi.string(word_eol[3]))
|
||||
exec_in_interp(python)
|
||||
|
||||
elif subcmd == 'load':
|
||||
filename = __decode(ffi.string(word[3]))
|
||||
load_filename(filename)
|
||||
|
||||
elif subcmd == 'unload':
|
||||
name = __decode(ffi.string(word[3]))
|
||||
if not unload_name(name):
|
||||
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
|
||||
|
||||
elif subcmd == 'reload':
|
||||
name = __decode(ffi.string(word[3]))
|
||||
if not reload_name(name):
|
||||
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
|
||||
|
||||
elif subcmd == 'console':
|
||||
lib.hexchat_command(lib.ph, b'QUERY >>python<<')
|
||||
|
||||
elif subcmd == 'list':
|
||||
list_plugins()
|
||||
|
||||
elif subcmd == 'about':
|
||||
lib.hexchat_print(lib.ph, b'HexChat Python interface version ' + VERSION)
|
||||
|
||||
else:
|
||||
lib.hexchat_command(lib.ph, b'HELP PY')
|
||||
|
||||
return 3
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_plugin_init(plugin_name, plugin_desc, plugin_version, arg, libdir):
|
||||
global hexchat
|
||||
global hexchat_stdout
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
plugin_name[0] = PLUGIN_NAME
|
||||
plugin_desc[0] = PLUGIN_DESC
|
||||
plugin_version[0] = PLUGIN_VERSION
|
||||
|
||||
try:
|
||||
libdir = __decode(ffi.string(libdir))
|
||||
modpath = os.path.join(libdir, '..', 'python')
|
||||
sys.path.append(os.path.abspath(modpath))
|
||||
hexchat = importlib.import_module('hexchat')
|
||||
|
||||
except (UnicodeDecodeError, ImportError) as e:
|
||||
lib.hexchat_print(lib.ph, b'Failed to import module: ' + repr(e).encode())
|
||||
|
||||
return 0
|
||||
|
||||
hexchat_stdout = Stdout()
|
||||
sys.stdout = hexchat_stdout
|
||||
sys.stderr = hexchat_stdout
|
||||
pydoc.help = pydoc.Helper(HelpEater(), HelpEater())
|
||||
|
||||
lib.hexchat_hook_command(lib.ph, b'', 0, lib._on_say_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'LOAD', 0, lib._on_load_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'UNLOAD', 0, lib._on_unload_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'RELOAD', 0, lib._on_reload_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'PY', 0, lib._on_py_command, b'''Usage: /PY LOAD <filename>
|
||||
UNLOAD <filename|name>
|
||||
RELOAD <filename|name>
|
||||
LIST
|
||||
EXEC <command>
|
||||
CONSOLE
|
||||
ABOUT''', ffi.NULL)
|
||||
|
||||
lib.hexchat_print(lib.ph, b'Python interface loaded')
|
||||
autoload()
|
||||
return 1
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_plugin_deinit():
|
||||
global local_interp
|
||||
global hexchat
|
||||
global hexchat_stdout
|
||||
global plugins
|
||||
|
||||
plugins = set()
|
||||
local_interp = None
|
||||
hexchat = None
|
||||
hexchat_stdout = None
|
||||
sys.stdout = sys.__stdout__
|
||||
sys.stderr = sys.__stderr__
|
||||
pydoc.help = pydoc.Helper()
|
||||
|
||||
for mod in ('_hexchat', 'hexchat', 'xchat', '_hexchat_embedded'):
|
||||
try:
|
||||
del sys.modules[mod]
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return 1
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@ -37,6 +37,9 @@
|
||||
<AdditionalDependencies>"$(Python2Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python2Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
@ -48,12 +51,15 @@
|
||||
<AdditionalDependencies>"$(Python2Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python2Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="python.def" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="python.c" />
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@ -37,6 +37,9 @@
|
||||
<AdditionalDependencies>"$(Python3Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python3Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
@ -48,12 +51,20 @@
|
||||
<AdditionalDependencies>"$(Python3Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python3Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="generate_plugin.py" />
|
||||
<None Include="hexchat.py" />
|
||||
<None Include="python.def" />
|
||||
<None Include="python.py" />
|
||||
<None Include="xchat.py" />
|
||||
<None Include="_hexchat.py" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="python.c" />
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
</Project>
|
@ -9,13 +9,26 @@
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="python.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="python.def">
|
||||
<Filter>Resource Files</Filter>
|
||||
</None>
|
||||
<None Include="_hexchat.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="generate_plugin.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="hexchat.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="python.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="xchat.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
26
plugins/python/python_style_guide.md
Normal file
26
plugins/python/python_style_guide.md
Normal file
@ -0,0 +1,26 @@
|
||||
# HexChat Python Module Style Guide
|
||||
|
||||
(This is a work in progress).
|
||||
|
||||
## General rules
|
||||
|
||||
- PEP8 as general fallback recommendations
|
||||
- Max line length: 120
|
||||
- Avoid overcomplex compound statements. i.e. dont do this: `somevar = x if x == y else z if a == b and c == b else x`
|
||||
|
||||
## Indentation style
|
||||
|
||||
### Multi-line functions
|
||||
|
||||
```python
|
||||
foo(really_long_arg_1,
|
||||
really_long_arg_2)
|
||||
```
|
||||
|
||||
### Mutli-line lists/dicts
|
||||
|
||||
```python
|
||||
foo = {
|
||||
'bar': 'baz',
|
||||
}
|
||||
```
|
1
plugins/python/xchat.py
Normal file
1
plugins/python/xchat.py
Normal file
@ -0,0 +1 @@
|
||||
from _hexchat import *
|
@ -13,13 +13,13 @@ sysinfo_includes = []
|
||||
sysinfo_cargs = []
|
||||
|
||||
system = host_machine.system()
|
||||
if system == 'linux' or system == 'darwin'
|
||||
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'darwin' or system == 'freebsd'
|
||||
sysinfo_includes += 'shared'
|
||||
sysinfo_sources += [
|
||||
'shared/df.c'
|
||||
]
|
||||
|
||||
if system == 'linux'
|
||||
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'freebsd'
|
||||
libpci = dependency('libpci', required: false, method: 'pkg-config')
|
||||
if libpci.found()
|
||||
sysinfo_deps += libpci
|
||||
@ -57,4 +57,5 @@ shared_module('sysinfo', sysinfo_sources,
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'sysinfo.def',
|
||||
)
|
||||
|
@ -26,7 +26,7 @@ int xs_parse_df(gint64 *out_total, gint64 *out_free)
|
||||
FILE *pipe;
|
||||
char buffer[bsize];
|
||||
|
||||
pipe = popen("df -k -l -P", "r");
|
||||
pipe = popen("df -k -l -P --exclude-type=squashfs --exclude-type=devtmpfs --exclude-type=tmpfs", "r");
|
||||
if(pipe==NULL)
|
||||
return 1;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
|
@ -269,6 +269,16 @@ int xs_parse_meminfo(unsigned long long *mem_tot, unsigned long long *mem_free,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void strip_quotes(char *string)
|
||||
{
|
||||
size_t len = strlen(string);
|
||||
if (string[len - 1] == '"')
|
||||
string[--len] = '\0';
|
||||
|
||||
if (string[0] == '"')
|
||||
memmove(string, string + 1, len);
|
||||
}
|
||||
|
||||
int xs_parse_distro(char *name)
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
@ -320,6 +330,20 @@ int xs_parse_distro(char *name)
|
||||
else
|
||||
g_snprintf(buffer, bsize, "Gentoo Linux %s", keywords);
|
||||
}
|
||||
else if((fp = fopen("/etc/os-release", "r")) != NULL)
|
||||
{
|
||||
char name[bsize], version[bsize];
|
||||
strcpy(name, "?");
|
||||
strcpy(version, "?");
|
||||
while(fgets(buffer, bsize, fp) != NULL)
|
||||
{
|
||||
find_match_char(buffer, "NAME=", name);
|
||||
find_match_char(buffer, "VERSION=", version);
|
||||
}
|
||||
strip_quotes(name);
|
||||
strip_quotes(version);
|
||||
g_snprintf(buffer, bsize, "%s %s", name, version);
|
||||
}
|
||||
else
|
||||
g_snprintf(buffer, bsize, "Unknown Distro");
|
||||
if(fp != NULL)
|
||||
|
@ -142,7 +142,7 @@ void pci_find_fullname(char *fullname, char *vendor, char *device)
|
||||
{
|
||||
position = strstr(buffer, vendor);
|
||||
position += 6;
|
||||
strncpy(vendorname, position, bsize/2);
|
||||
g_strlcpy(vendorname, position, sizeof (vendorname));
|
||||
position = strstr(vendorname, "\n");
|
||||
*(position) = '\0';
|
||||
break;
|
||||
@ -154,7 +154,7 @@ void pci_find_fullname(char *fullname, char *vendor, char *device)
|
||||
{
|
||||
position = strstr(buffer, device);
|
||||
position += 6;
|
||||
strncpy(devicename, position, bsize/2);
|
||||
g_strlcpy(devicename, position, sizeof (devicename));
|
||||
position = strstr(devicename, " (");
|
||||
if (position == NULL)
|
||||
position = strstr(devicename, "\n");
|
||||
|
@ -5,4 +5,5 @@ shared_module('upd', 'upd.c',
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'upd.def',
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -3,4 +3,5 @@ shared_module('winamp', 'winamp.c',
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
vs_module_defs: 'winamp.def',
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
636
po/en_GB.po
636
po/en_GB.po
File diff suppressed because it is too large
Load Diff
684
po/ja_JP.po
684
po/ja_JP.po
File diff suppressed because it is too large
Load Diff
@ -1 +1,9 @@
|
||||
i18n.gettext('hexchat', preset: 'glib')
|
||||
|
||||
validate_translations = find_program('validate-textevent-translations')
|
||||
test('Validate translations', validate_translations,
|
||||
args: [
|
||||
join_paths(meson.current_source_dir(), 'LINGUAS'),
|
||||
meson.current_source_dir(),
|
||||
]
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user