79 changes: 79 additions & 0 deletions flatpak/io.github.Hexchat.json
@@ -0,0 +1,79 @@
{
"app-id": "io.github.Hexchat",
"branch": "stable",
"runtime": "org.gnome.Platform",
"runtime-version": "40",
"sdk": "org.gnome.Sdk",
"command": "hexchat",
"rename-icon": "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-0.110.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 changes: 19 additions & 0 deletions flatpak/python3-cffi.json
@@ -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 change: 1 addition & 0 deletions flatpak/shared-modules
Submodule shared-modules added at 45cc38
70 changes: 55 additions & 15 deletions meson.build
@@ -1,6 +1,6 @@
project('hexchat', 'c',
version: '2.14.2',
meson_version: '>= 0.38.0',
version: '2.16.0',
meson_version: '>= 0.47.0',
default_options: [
'c_std=gnu89',
'buildtype=debugoptimized',
Expand All @@ -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()
Expand All @@ -32,11 +37,10 @@ config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
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)
Expand All @@ -49,6 +53,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)
Expand Down Expand Up @@ -87,8 +95,6 @@ endif

global_cflags = []
test_cflags = [
'-pipe',
'-fPIE',
'-funsigned-char',
'-Wno-conversion',
'-Wno-pointer-sign',
Expand Down Expand Up @@ -132,25 +138,59 @@ test_ldflags = [
'-Wl,-z,relro',
'-Wl,-z,now',
# 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'
subdir('data')
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
37 changes: 18 additions & 19 deletions meson_options.txt
@@ -1,36 +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',
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('with-plugin', type: 'boolean',
option('plugin', type: 'boolean',
description: 'Support for loadable plugins'
)
option('with-dbus', type: 'boolean',
option('dbus', type: 'feature', value: 'auto',
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',
option('libcanberra', type: 'feature', value: 'auto',
description: 'Support for sound alerts, Unix only'
)
option('with-theme-manager', type: 'boolean', value: false,
description: 'Utility to help manage themes, requires mono/.net'
)

# Install options
option('dbus-service-use-appid', type: 'boolean', value: false,
description: 'Rename dbus service to match app-id, required for Flatpak'
)
option('with-appdata', type: 'boolean',
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',
Expand Down Expand Up @@ -60,9 +62,6 @@ option('with-upd', type: 'boolean',
option('with-winamp', type: 'boolean',
description: 'Winamp plugin, Windows only'
)
option('install-plugin-metainfo', type: 'boolean', value: false,
description: 'Installs metainfo files for enabled plugins, useful when distros create split packages'
)
option('with-perl-legacy-api', type: 'boolean', value: false,
description: 'Enables the legacy IRC perl module for compatibility with old scripts'
)
2 changes: 1 addition & 1 deletion plugins/checksum/checksum.vcxproj
@@ -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">
Expand Down
2 changes: 1 addition & 1 deletion plugins/exec/exec.vcxproj
@@ -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">
Expand Down
513 changes: 411 additions & 102 deletions plugins/fishlim/fish.c

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions plugins/fishlim/fish.h
@@ -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
Expand Down Expand Up @@ -29,10 +30,19 @@

#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
};

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

Expand Down
8 changes: 5 additions & 3 deletions plugins/fishlim/fishlim.vcxproj
@@ -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">
Expand Down Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -55,13 +55,15 @@
<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" />
</ItemGroup>
<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" />
Expand Down
12 changes: 12 additions & 0 deletions plugins/fishlim/fishlim.vcxproj.filters
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
61 changes: 46 additions & 15 deletions plugins/fishlim/keystore.c
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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;
Expand All @@ -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 */
Expand All @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions plugins/fishlim/keystore.h
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions plugins/fishlim/meson.build
Expand Up @@ -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'
Expand Down
568 changes: 413 additions & 155 deletions plugins/fishlim/plugin_hexchat.c

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions plugins/fishlim/tests/meson.build
@@ -0,0 +1,15 @@
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',
)
51 changes: 51 additions & 0 deletions plugins/fishlim/tests/mock-keystore.c
@@ -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;
}
286 changes: 286 additions & 0 deletions plugins/fishlim/tests/tests.c
@@ -0,0 +1,286 @@
/*
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 <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;
int i, message_len = 0;
char message[1000];

for (i = 0; i < 10; ++i) {
for (message_len = 1; message_len < 1000; ++message_len) {
random_string(message, message_len);
b64 = g_base64_encode((const unsigned char *) message, message_len);
g_assert_nonnull(b64);
g_assert_cmpuint(strlen(b64), == , base64_len(message_len));
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 i, message_len = 0;
char message[1000];

for (i = 0; i < 10; ++i) {

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 tests, max_chunks_len, chunks_len;
char ascii_message[1001];
char *data_chunk = NULL;

rand = g_rand_new();

for (tests = 0; tests < 1000; ++tests) {

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);
}
}

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);

return g_test_run();
}
149 changes: 149 additions & 0 deletions plugins/fishlim/utils.c
@@ -0,0 +1,149 @@
/*
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 "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 changes: 39 additions & 0 deletions plugins/fishlim/utils.h
@@ -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
16 changes: 9 additions & 7 deletions plugins/lua/lua.c
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1187,11 +1189,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;
Expand Down
2 changes: 1 addition & 1 deletion plugins/lua/lua.vcxproj
@@ -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">
Expand Down
4 changes: 3 additions & 1 deletion plugins/perl/perl.c
Expand Up @@ -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[])
{
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion plugins/perl/perl.vcxproj
@@ -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">
Expand Down
386 changes: 386 additions & 0 deletions plugins/python/_hexchat.py
@@ -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 changes: 89 additions & 0 deletions plugins/python/generate_plugin.py
@@ -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 change: 1 addition & 0 deletions plugins/python/hexchat.py
@@ -0,0 +1 @@
from _hexchat import *
24 changes: 21 additions & 3 deletions plugins/python/meson.build
@@ -1,12 +1,30 @@
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: '',
Expand Down
2,834 changes: 0 additions & 2,834 deletions plugins/python/python.c

This file was deleted.

1 change: 0 additions & 1 deletion plugins/python/python.def
@@ -1,4 +1,3 @@
EXPORTS
hexchat_plugin_init
hexchat_plugin_deinit
hexchat_plugin_get_info
554 changes: 554 additions & 0 deletions plugins/python/python.py

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions plugins/python/python2.vcxproj
@@ -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">
Expand Down Expand Up @@ -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>
Expand All @@ -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>
17 changes: 14 additions & 3 deletions plugins/python/python3.vcxproj
@@ -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">
Expand Down Expand Up @@ -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>
Expand All @@ -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>
19 changes: 16 additions & 3 deletions plugins/python/python3.vcxproj.filters
Expand Up @@ -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 changes: 26 additions & 0 deletions plugins/python/python_style_guide.md
@@ -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 change: 1 addition & 0 deletions plugins/python/xchat.py
@@ -0,0 +1 @@
from _hexchat import *
4 changes: 2 additions & 2 deletions plugins/sysinfo/meson.build
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugins/sysinfo/shared/df.c
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion plugins/sysinfo/sysinfo.vcxproj
@@ -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>
Expand Down
24 changes: 24 additions & 0 deletions plugins/sysinfo/unix/parse.c
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions plugins/sysinfo/unix/pci.c
Expand Up @@ -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;
Expand All @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion plugins/upd/upd.vcxproj
@@ -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">
Expand Down
2 changes: 1 addition & 1 deletion plugins/winamp/winamp.vcxproj
@@ -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">
Expand Down
124 changes: 64 additions & 60 deletions po/af.po
@@ -1,22 +1,21 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the hexchat package.
#
#
# Translators:
# Petri Jooste <rkwjpj@puk.ac.za>, 2004
msgid ""
msgstr ""
"Project-Id-Version: HexChat\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-03 16:11-0400\n"
"PO-Revision-Date: 2017-09-19 14:16+0000\n"
"POT-Creation-Date: 2018-04-03 16:10-0400\n"
"PO-Revision-Date: 2018-04-03 20:10+0000\n"
"Last-Translator: TingPing <tingping@tingping.se>\n"
"Language-Team: Afrikaans (http://www.transifex.com/hexchat/hexchat/language/"
"af/)\n"
"Language: af\n"
"Language-Team: Afrikaans (http://www.transifex.com/hexchat/hexchat/language/af/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: af\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: data/misc/io.github.Hexchat.appdata.xml.in:4
Expand Down Expand Up @@ -298,9 +297,7 @@ msgstr ""
msgid ""
"* Running IRC as root is stupid! You should\n"
" create a User Account and use that to login.\n"
msgstr ""
"* Om IRC as root te loop is onnosel! Jy moet\n"
" 'n gebruiker-ID skep en dit gebruik om aan te teken.\n"
msgstr "* Om IRC as root te loop is onnosel! Jy moet\n 'n gebruiker-ID skep en dit gebruik om aan te teken.\n"

#: src/common/ignore.c:127 src/common/ignore.c:131 src/common/ignore.c:135
#: src/common/ignore.c:139 src/common/ignore.c:143 src/common/ignore.c:147
Expand Down Expand Up @@ -460,7 +457,8 @@ msgstr ""

#: src/common/outbound.c:3945
msgid ""
"CHARSET [<encoding>], get or set the encoding used for the current connection"
"CHARSET [<encoding>], get or set the encoding used for the current "
"connection"
msgstr ""

#: src/common/outbound.c:3946
Expand All @@ -487,7 +485,8 @@ msgstr ""

#: src/common/outbound.c:3954
msgid ""
"CYCLE [<channel>], parts the current or given channel and immediately rejoins"
"CYCLE [<channel>], parts the current or given channel and immediately "
"rejoins"
msgstr ""

#: src/common/outbound.c:3956
Expand Down Expand Up @@ -629,8 +628,7 @@ msgid ""
" Use -h to highlight the found string(s)\n"
" Use -m to match case\n"
" Use -r when string is a Regular Expression\n"
" Use -- (double hyphen) to end options when searching for, say, the "
"string '-r'"
" Use -- (double hyphen) to end options when searching for, say, the string '-r'"
msgstr ""

#: src/common/outbound.c:4034
Expand All @@ -648,8 +646,8 @@ msgstr ""

#: src/common/outbound.c:4041
msgid ""
"ME <action>, sends the action to the current channel (actions are written in "
"the 3rd person, like /me jumps)"
"ME <action>, sends the action to the current channel (actions are written in"
" the 3rd person, like /me jumps)"
msgstr ""

#: src/common/outbound.c:4045
Expand All @@ -667,8 +665,8 @@ msgstr ""

#: src/common/outbound.c:4051
msgid ""
"MSG <nick> <message>, sends a private message, message \".\" to send to last "
"nick or prefix with \"=\" for dcc chat"
"MSG <nick> <message>, sends a private message, message \".\" to send to last"
" nick or prefix with \"=\" for dcc chat"
msgstr ""

#: src/common/outbound.c:4054
Expand Down Expand Up @@ -732,8 +730,8 @@ msgstr ""

#: src/common/outbound.c:4080
msgid ""
"RECONNECT [-ssl] [<host>] [<port>] [<password>], Can be called just as /"
"RECONNECT to reconnect to the current server or with /RECONNECT ALL to "
"RECONNECT [-ssl] [<host>] [<port>] [<password>], Can be called just as "
"/RECONNECT to reconnect to the current server or with /RECONNECT ALL to "
"reconnect to all the open servers"
msgstr ""

Expand Down Expand Up @@ -800,7 +798,8 @@ msgstr ""

#: src/common/outbound.c:4110
msgid ""
"TOPIC [<topic>], sets the topic if one is given, else shows the current topic"
"TOPIC [<topic>], sets the topic if one is given, else shows the current "
"topic"
msgstr ""

#: src/common/outbound.c:4112
Expand All @@ -827,8 +826,8 @@ msgstr ""

#: src/common/outbound.c:4123
msgid ""
"UNQUIET <mask> [<mask>...], unquiets the specified masks if supported by the "
"server."
"UNQUIET <mask> [<mask>...], unquiets the specified masks if supported by the"
" server."
msgstr ""

#: src/common/outbound.c:4124
Expand All @@ -837,7 +836,8 @@ msgstr ""

#: src/common/outbound.c:4126
msgid ""
"USELECT [-a] [-s] <nick1> <nick2> etc, highlights nick(s) in channel userlist"
"USELECT [-a] [-s] <nick1> <nick2> etc, highlights nick(s) in channel "
"userlist"
msgstr ""

#: src/common/outbound.c:4129
Expand Down Expand Up @@ -1111,7 +1111,8 @@ msgid "%C23*%O$tDCC CHAT to %C18$1%O aborted."
msgstr ""

#: src/common/textevents.h:145
msgid "%C24*%O$tDCC CHAT connection established to %C18$1%C %C30[%C24$2%C30]%O"
msgid ""
"%C24*%O$tDCC CHAT connection established to %C18$1%C %C30[%C24$2%C30]%O"
msgstr ""

#: src/common/textevents.h:148
Expand Down Expand Up @@ -1145,8 +1146,8 @@ msgstr ""

#: src/common/textevents.h:169
msgid ""
"%C20*%O$tReceived a malformed DCC request from %C18$1%O.$a010%C23*%O"
"$tContents of packet: %C23$2%O"
"%C20*%O$tReceived a malformed DCC request from "
"%C18$1%O.$a010%C23*%O$tContents of packet: %C23$2%O"
msgstr ""

#: src/common/textevents.h:172
Expand All @@ -1167,7 +1168,8 @@ msgid ""
msgstr ""

#: src/common/textevents.h:184
msgid "%C24*%O$tDCC RECV connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgid ""
"%C24*%O$tDCC RECV connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgstr ""

#: src/common/textevents.h:187
Expand All @@ -1180,7 +1182,8 @@ msgstr ""

#: src/common/textevents.h:193
msgid ""
"%C23*%O$tThe file '%C24$1%C' already exists, saving it as '%C23$2%O' instead."
"%C23*%O$tThe file '%C24$1%C' already exists, saving it as '%C23$2%O' "
"instead."
msgstr ""

#: src/common/textevents.h:196
Expand All @@ -1197,7 +1200,8 @@ msgid ""
msgstr ""

#: src/common/textevents.h:205
msgid "%C24*%O$tDCC SEND connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgid ""
"%C24*%O$tDCC SEND connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgstr ""

#: src/common/textevents.h:208
Expand Down Expand Up @@ -3502,18 +3506,18 @@ msgstr ""

#: src/fe-gtk/fkeys.c:141
msgid ""
"The Run Command action runs the data in Data 1 as if it had been typed into "
"the entry box where you pressed the key sequence. Thus it can contain text "
"(which will be sent to the channel/person), commands or user commands. When "
"run all \\n characters in Data 1 are used to deliminate separate commands so "
"it is possible to run more than one command. If you want a \\ in the actual "
"text run then enter \\\\"
"The Run Command action runs the data in Data 1 as if it had been typed "
"into the entry box where you pressed the key sequence. Thus it can contain "
"text (which will be sent to the channel/person), commands or user commands. "
"When run all \\n characters in Data 1 are used to deliminate separate "
"commands so it is possible to run more than one command. If you want a \\ "
"in the actual text run then enter \\\\"
msgstr ""

#: src/fe-gtk/fkeys.c:143
msgid ""
"The Change Page command switches between pages in the notebook. Set Data 1 "
"to the page you want to switch to. If Data 2 is set to anything then the "
"The Change Page command switches between pages in the notebook. Set Data 1"
" to the page you want to switch to. If Data 2 is set to anything then the "
"switch will be relative to the current position. Set Data 1 to auto to "
"switch to the page with the most recent and important activity (queries "
"first, then channels with hilight, channels with dialogue, channels with "
Expand All @@ -3528,39 +3532,39 @@ msgstr ""

#: src/fe-gtk/fkeys.c:147
msgid ""
"The Scroll Page command scrolls the text widget up or down one page or one "
"line. Set Data 1 to either Top, Bottom, Up, Down, +1 or -1."
"The Scroll Page command scrolls the text widget up or down one page or one"
" line. Set Data 1 to either Top, Bottom, Up, Down, +1 or -1."
msgstr ""

#: src/fe-gtk/fkeys.c:149
msgid ""
"The Set Buffer command sets the entry where the key sequence was entered to "
"the contents of Data 1"
"The Set Buffer command sets the entry where the key sequence was entered "
"to the contents of Data 1"
msgstr ""

#: src/fe-gtk/fkeys.c:151
msgid ""
"The Last Command command sets the entry to contain the last command entered "
"- the same as pressing up in a shell"
"The Last Command command sets the entry to contain the last command "
"entered - the same as pressing up in a shell"
msgstr ""

#: src/fe-gtk/fkeys.c:153
msgid ""
"The Next Command command sets the entry to contain the next command entered "
"- the same as pressing down in a shell"
"The Next Command command sets the entry to contain the next command "
"entered - the same as pressing down in a shell"
msgstr ""

#: src/fe-gtk/fkeys.c:155
msgid ""
"This command changes the text in the entry to finish an incomplete nickname "
"or command. If Data 1 is set then double-tabbing in a string will select the "
"last nick, not the next"
"or command. If Data 1 is set then double-tabbing in a string will select the"
" last nick, not the next"
msgstr ""

#: src/fe-gtk/fkeys.c:157
msgid ""
"This command scrolls up and down through the list of nicks. If Data 1 is set "
"to anything it will scroll up, else it scrolls down"
"This command scrolls up and down through the list of nicks. If Data 1 is set"
" to anything it will scroll up, else it scrolls down"
msgstr ""

#: src/fe-gtk/fkeys.c:159
Expand Down Expand Up @@ -5694,8 +5698,8 @@ msgstr ""

#: src/fe-gtk/setup.c:562
msgid ""
"Automatically include timestamps in copied lines of text. Otherwise, include "
"timestamps if the Shift key is held down while selecting."
"Automatically include timestamps in copied lines of text. Otherwise, include"
" timestamps if the Shift key is held down while selecting."
msgstr ""

#: src/fe-gtk/setup.c:564
Expand All @@ -5704,8 +5708,8 @@ msgstr ""

#: src/fe-gtk/setup.c:565
msgid ""
"Automatically include color information in copied lines of text. Otherwise, "
"include color information if the Ctrl key is held down while selecting."
"Automatically include color information in copied lines of text. Otherwise,"
" include color information if the Ctrl key is held down while selecting."
msgstr ""

#: src/fe-gtk/setup.c:570
Expand Down Expand Up @@ -5756,7 +5760,8 @@ msgstr ""

#: src/fe-gtk/setup.c:579
msgid ""
"Attempt to use this banmask when banning or quieting. (requires irc_who_join)"
"Attempt to use this banmask when banning or quieting. (requires "
"irc_who_join)"
msgstr ""

#: src/fe-gtk/setup.c:586 src/fe-gtk/setup.c:1883
Expand Down Expand Up @@ -5866,8 +5871,8 @@ msgstr ""

#: src/fe-gtk/setup.c:639
msgid ""
"Asks the IRC server for your real address. Use this if you have a 192.168.*."
"* address!"
"Asks the IRC server for your real address. Use this if you have a "
"192.168.*.* address!"
msgstr ""

#: src/fe-gtk/setup.c:640
Expand Down Expand Up @@ -6103,8 +6108,7 @@ msgid ""
msgstr ""

#: src/fe-gtk/setup.c:2254
msgid ""
"The Real name option cannot be left blank. Falling back to \"realname\"."
msgid "The Real name option cannot be left blank. Falling back to \"realname\"."
msgstr ""

#: src/fe-gtk/setup.c:2261
Expand Down Expand Up @@ -6231,8 +6235,8 @@ msgstr ""
#: plugins/sysinfo/sysinfo.c:168
msgid ""
"Sysinfo: Valid settings are: announce and hide_* for each piece of "
"information. e.g. hide_os. Without a value it will show current (or default) "
"setting.\n"
"information. e.g. hide_os. Without a value it will show current (or default)"
" setting.\n"
msgstr ""

#: plugins/sysinfo/sysinfo.c:190
Expand Down
120 changes: 63 additions & 57 deletions po/am.po
@@ -1,22 +1,21 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the hexchat package.
#
#
# Translators:
# Ge'ez Frontier Foundation <locales@geez.org>, 2002
msgid ""
msgstr ""
"Project-Id-Version: HexChat\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-03 16:11-0400\n"
"PO-Revision-Date: 2017-09-19 14:16+0000\n"
"POT-Creation-Date: 2018-04-03 16:10-0400\n"
"PO-Revision-Date: 2018-04-03 20:10+0000\n"
"Last-Translator: TingPing <tingping@tingping.se>\n"
"Language-Team: Amharic (http://www.transifex.com/hexchat/hexchat/language/"
"am/)\n"
"Language: am\n"
"Language-Team: Amharic (http://www.transifex.com/hexchat/hexchat/language/am/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: am\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

#: data/misc/io.github.Hexchat.appdata.xml.in:4
Expand Down Expand Up @@ -458,7 +457,8 @@ msgstr ""

#: src/common/outbound.c:3945
msgid ""
"CHARSET [<encoding>], get or set the encoding used for the current connection"
"CHARSET [<encoding>], get or set the encoding used for the current "
"connection"
msgstr ""

#: src/common/outbound.c:3946
Expand All @@ -485,7 +485,8 @@ msgstr ""

#: src/common/outbound.c:3954
msgid ""
"CYCLE [<channel>], parts the current or given channel and immediately rejoins"
"CYCLE [<channel>], parts the current or given channel and immediately "
"rejoins"
msgstr ""

#: src/common/outbound.c:3956
Expand Down Expand Up @@ -627,8 +628,7 @@ msgid ""
" Use -h to highlight the found string(s)\n"
" Use -m to match case\n"
" Use -r when string is a Regular Expression\n"
" Use -- (double hyphen) to end options when searching for, say, the "
"string '-r'"
" Use -- (double hyphen) to end options when searching for, say, the string '-r'"
msgstr ""

#: src/common/outbound.c:4034
Expand All @@ -646,8 +646,8 @@ msgstr ""

#: src/common/outbound.c:4041
msgid ""
"ME <action>, sends the action to the current channel (actions are written in "
"the 3rd person, like /me jumps)"
"ME <action>, sends the action to the current channel (actions are written in"
" the 3rd person, like /me jumps)"
msgstr ""

#: src/common/outbound.c:4045
Expand All @@ -665,8 +665,8 @@ msgstr ""

#: src/common/outbound.c:4051
msgid ""
"MSG <nick> <message>, sends a private message, message \".\" to send to last "
"nick or prefix with \"=\" for dcc chat"
"MSG <nick> <message>, sends a private message, message \".\" to send to last"
" nick or prefix with \"=\" for dcc chat"
msgstr ""

#: src/common/outbound.c:4054
Expand Down Expand Up @@ -730,8 +730,8 @@ msgstr ""

#: src/common/outbound.c:4080
msgid ""
"RECONNECT [-ssl] [<host>] [<port>] [<password>], Can be called just as /"
"RECONNECT to reconnect to the current server or with /RECONNECT ALL to "
"RECONNECT [-ssl] [<host>] [<port>] [<password>], Can be called just as "
"/RECONNECT to reconnect to the current server or with /RECONNECT ALL to "
"reconnect to all the open servers"
msgstr ""

Expand Down Expand Up @@ -798,7 +798,8 @@ msgstr ""

#: src/common/outbound.c:4110
msgid ""
"TOPIC [<topic>], sets the topic if one is given, else shows the current topic"
"TOPIC [<topic>], sets the topic if one is given, else shows the current "
"topic"
msgstr ""

#: src/common/outbound.c:4112
Expand All @@ -825,8 +826,8 @@ msgstr ""

#: src/common/outbound.c:4123
msgid ""
"UNQUIET <mask> [<mask>...], unquiets the specified masks if supported by the "
"server."
"UNQUIET <mask> [<mask>...], unquiets the specified masks if supported by the"
" server."
msgstr ""

#: src/common/outbound.c:4124
Expand All @@ -835,7 +836,8 @@ msgstr ""

#: src/common/outbound.c:4126
msgid ""
"USELECT [-a] [-s] <nick1> <nick2> etc, highlights nick(s) in channel userlist"
"USELECT [-a] [-s] <nick1> <nick2> etc, highlights nick(s) in channel "
"userlist"
msgstr ""

#: src/common/outbound.c:4129
Expand Down Expand Up @@ -1109,7 +1111,8 @@ msgid "%C23*%O$tDCC CHAT to %C18$1%O aborted."
msgstr ""

#: src/common/textevents.h:145
msgid "%C24*%O$tDCC CHAT connection established to %C18$1%C %C30[%C24$2%C30]%O"
msgid ""
"%C24*%O$tDCC CHAT connection established to %C18$1%C %C30[%C24$2%C30]%O"
msgstr ""

#: src/common/textevents.h:148
Expand Down Expand Up @@ -1143,8 +1146,8 @@ msgstr ""

#: src/common/textevents.h:169
msgid ""
"%C20*%O$tReceived a malformed DCC request from %C18$1%O.$a010%C23*%O"
"$tContents of packet: %C23$2%O"
"%C20*%O$tReceived a malformed DCC request from "
"%C18$1%O.$a010%C23*%O$tContents of packet: %C23$2%O"
msgstr ""

#: src/common/textevents.h:172
Expand All @@ -1165,7 +1168,8 @@ msgid ""
msgstr ""

#: src/common/textevents.h:184
msgid "%C24*%O$tDCC RECV connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgid ""
"%C24*%O$tDCC RECV connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgstr ""

#: src/common/textevents.h:187
Expand All @@ -1178,7 +1182,8 @@ msgstr ""

#: src/common/textevents.h:193
msgid ""
"%C23*%O$tThe file '%C24$1%C' already exists, saving it as '%C23$2%O' instead."
"%C23*%O$tThe file '%C24$1%C' already exists, saving it as '%C23$2%O' "
"instead."
msgstr ""

#: src/common/textevents.h:196
Expand All @@ -1195,7 +1200,8 @@ msgid ""
msgstr ""

#: src/common/textevents.h:205
msgid "%C24*%O$tDCC SEND connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgid ""
"%C24*%O$tDCC SEND connection established to %C18$1 %C30[%O%C24$2%C30]%O"
msgstr ""

#: src/common/textevents.h:208
Expand Down Expand Up @@ -3500,18 +3506,18 @@ msgstr ""

#: src/fe-gtk/fkeys.c:141
msgid ""
"The Run Command action runs the data in Data 1 as if it had been typed into "
"the entry box where you pressed the key sequence. Thus it can contain text "
"(which will be sent to the channel/person), commands or user commands. When "
"run all \\n characters in Data 1 are used to deliminate separate commands so "
"it is possible to run more than one command. If you want a \\ in the actual "
"text run then enter \\\\"
"The Run Command action runs the data in Data 1 as if it had been typed "
"into the entry box where you pressed the key sequence. Thus it can contain "
"text (which will be sent to the channel/person), commands or user commands. "
"When run all \\n characters in Data 1 are used to deliminate separate "
"commands so it is possible to run more than one command. If you want a \\ "
"in the actual text run then enter \\\\"
msgstr ""

#: src/fe-gtk/fkeys.c:143
msgid ""
"The Change Page command switches between pages in the notebook. Set Data 1 "
"to the page you want to switch to. If Data 2 is set to anything then the "
"The Change Page command switches between pages in the notebook. Set Data 1"
" to the page you want to switch to. If Data 2 is set to anything then the "
"switch will be relative to the current position. Set Data 1 to auto to "
"switch to the page with the most recent and important activity (queries "
"first, then channels with hilight, channels with dialogue, channels with "
Expand All @@ -3526,39 +3532,39 @@ msgstr ""

#: src/fe-gtk/fkeys.c:147
msgid ""
"The Scroll Page command scrolls the text widget up or down one page or one "
"line. Set Data 1 to either Top, Bottom, Up, Down, +1 or -1."
"The Scroll Page command scrolls the text widget up or down one page or one"
" line. Set Data 1 to either Top, Bottom, Up, Down, +1 or -1."
msgstr ""

#: src/fe-gtk/fkeys.c:149
msgid ""
"The Set Buffer command sets the entry where the key sequence was entered to "
"the contents of Data 1"
"The Set Buffer command sets the entry where the key sequence was entered "
"to the contents of Data 1"
msgstr ""

#: src/fe-gtk/fkeys.c:151
msgid ""
"The Last Command command sets the entry to contain the last command entered "
"- the same as pressing up in a shell"
"The Last Command command sets the entry to contain the last command "
"entered - the same as pressing up in a shell"
msgstr ""

#: src/fe-gtk/fkeys.c:153
msgid ""
"The Next Command command sets the entry to contain the next command entered "
"- the same as pressing down in a shell"
"The Next Command command sets the entry to contain the next command "
"entered - the same as pressing down in a shell"
msgstr ""

#: src/fe-gtk/fkeys.c:155
msgid ""
"This command changes the text in the entry to finish an incomplete nickname "
"or command. If Data 1 is set then double-tabbing in a string will select the "
"last nick, not the next"
"or command. If Data 1 is set then double-tabbing in a string will select the"
" last nick, not the next"
msgstr ""

#: src/fe-gtk/fkeys.c:157
msgid ""
"This command scrolls up and down through the list of nicks. If Data 1 is set "
"to anything it will scroll up, else it scrolls down"
"This command scrolls up and down through the list of nicks. If Data 1 is set"
" to anything it will scroll up, else it scrolls down"
msgstr ""

#: src/fe-gtk/fkeys.c:159
Expand Down Expand Up @@ -5692,8 +5698,8 @@ msgstr ""

#: src/fe-gtk/setup.c:562
msgid ""
"Automatically include timestamps in copied lines of text. Otherwise, include "
"timestamps if the Shift key is held down while selecting."
"Automatically include timestamps in copied lines of text. Otherwise, include"
" timestamps if the Shift key is held down while selecting."
msgstr ""

#: src/fe-gtk/setup.c:564
Expand All @@ -5702,8 +5708,8 @@ msgstr ""

#: src/fe-gtk/setup.c:565
msgid ""
"Automatically include color information in copied lines of text. Otherwise, "
"include color information if the Ctrl key is held down while selecting."
"Automatically include color information in copied lines of text. Otherwise,"
" include color information if the Ctrl key is held down while selecting."
msgstr ""

#: src/fe-gtk/setup.c:570
Expand Down Expand Up @@ -5754,7 +5760,8 @@ msgstr ""

#: src/fe-gtk/setup.c:579
msgid ""
"Attempt to use this banmask when banning or quieting. (requires irc_who_join)"
"Attempt to use this banmask when banning or quieting. (requires "
"irc_who_join)"
msgstr ""

#: src/fe-gtk/setup.c:586 src/fe-gtk/setup.c:1883
Expand Down Expand Up @@ -5864,8 +5871,8 @@ msgstr ""

#: src/fe-gtk/setup.c:639
msgid ""
"Asks the IRC server for your real address. Use this if you have a 192.168.*."
"* address!"
"Asks the IRC server for your real address. Use this if you have a "
"192.168.*.* address!"
msgstr ""

#: src/fe-gtk/setup.c:640
Expand Down Expand Up @@ -6101,8 +6108,7 @@ msgid ""
msgstr ""

#: src/fe-gtk/setup.c:2254
msgid ""
"The Real name option cannot be left blank. Falling back to \"realname\"."
msgid "The Real name option cannot be left blank. Falling back to \"realname\"."
msgstr ""

#: src/fe-gtk/setup.c:2261
Expand Down Expand Up @@ -6229,8 +6235,8 @@ msgstr ""
#: plugins/sysinfo/sysinfo.c:168
msgid ""
"Sysinfo: Valid settings are: announce and hide_* for each piece of "
"information. e.g. hide_os. Without a value it will show current (or default) "
"setting.\n"
"information. e.g. hide_os. Without a value it will show current (or default)"
" setting.\n"
msgstr ""

#: plugins/sysinfo/sysinfo.c:190
Expand Down