summary refs log tree commit diff stats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/checksum/checksum.vcxproj2
-rw-r--r--plugins/exec/exec.vcxproj2
-rw-r--r--plugins/fishlim/fish.c513
-rw-r--r--plugins/fishlim/fish.h18
-rw-r--r--plugins/fishlim/fishlim.vcxproj4
-rw-r--r--plugins/fishlim/fishlim.vcxproj.filters12
-rw-r--r--plugins/fishlim/keystore.c61
-rw-r--r--plugins/fishlim/keystore.h5
-rw-r--r--plugins/fishlim/meson.build4
-rw-r--r--plugins/fishlim/plugin_hexchat.c566
-rw-r--r--plugins/fishlim/tests/fake/keystore.c47
-rw-r--r--plugins/fishlim/tests/meson.build17
-rw-r--r--plugins/fishlim/tests/old_version/fish.c155
-rw-r--r--plugins/fishlim/tests/old_version/fish.h37
-rw-r--r--plugins/fishlim/tests/old_version/meson.build4
-rw-r--r--plugins/fishlim/tests/tests.c297
-rw-r--r--plugins/fishlim/utils.c149
-rw-r--r--plugins/fishlim/utils.h39
-rw-r--r--plugins/lua/lua.c16
-rw-r--r--plugins/lua/lua.vcxproj2
-rw-r--r--plugins/perl/perl.c4
-rw-r--r--plugins/perl/perl.vcxproj2
-rw-r--r--plugins/python/_hexchat.py386
-rwxr-xr-xplugins/python/generate_plugin.py89
-rw-r--r--plugins/python/hexchat.py1
-rw-r--r--plugins/python/meson.build24
-rw-r--r--plugins/python/python.c2834
-rw-r--r--plugins/python/python.def1
-rw-r--r--plugins/python/python.py554
-rw-r--r--plugins/python/python2.vcxproj10
-rw-r--r--plugins/python/python3.vcxproj17
-rw-r--r--plugins/python/python3.vcxproj.filters19
-rw-r--r--plugins/python/python_style_guide.md26
-rw-r--r--plugins/python/xchat.py1
-rw-r--r--plugins/sysinfo/meson.build4
-rw-r--r--plugins/sysinfo/shared/df.c2
-rw-r--r--plugins/sysinfo/sysinfo.vcxproj2
-rw-r--r--plugins/upd/upd.vcxproj2
-rw-r--r--plugins/winamp/winamp.vcxproj2
39 files changed, 2789 insertions, 3141 deletions
diff --git a/plugins/checksum/checksum.vcxproj b/plugins/checksum/checksum.vcxproj
index 7924be88..fba08cce 100644
--- a/plugins/checksum/checksum.vcxproj
+++ b/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">

diff --git a/plugins/exec/exec.vcxproj b/plugins/exec/exec.vcxproj
index e34f10e6..ceb11843 100644
--- a/plugins/exec/exec.vcxproj
+++ b/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">

diff --git a/plugins/fishlim/fish.c b/plugins/fishlim/fish.c
index 00ecfbfa..c2c2b9da 100644
--- a/plugins/fishlim/fish.c
+++ b/plugins/fishlim/fish.c
@@ -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,430 @@ 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;
+/**
+ * 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);
         }
-        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;
-            }
+
+        for (i = 0; i < 6; ++i) {
+            *end++ = fish_base64[left & 0x3fu];
+            left = (left >> 6u);
         }
-        
-        /* Stop if a null terminator was found */
-        if (c == '\0') break;
+
+        /* The previous for'd ensure fill all bytes of encoded, we don't need will with zeros */
+        message += 8;
     }
+
     *end = '\0';
-    return encrypted;
+    return encoded;
 }
 
+/**
+ * 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_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;
 
-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;
-            }
+    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;
+}
+
+/**
+ * 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_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;
         }
-        
-        /* Decrypt block */
-        BF_decrypt(binary, &bfkey);
-        
-        /* Copy to buffer */
-        GET_BYTES(end, binary[0]);
-        GET_BYTES(end, binary[1]);
+
+        cipher = (EVP_CIPHER *) EVP_bf_cbc();
+    } else if (mode == EVP_CIPH_ECB_MODE) {
+        cipher = (EVP_CIPHER *) EVP_bf_ecb();
+    }
+
+    /* 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;
     }
-    
-  decrypt_end:
-    *end = '\0';
-    return decrypted;
 }
 
 /**
- * Encrypts a message (see fish_decrypt). The key is searched for in the
- * key store.
+ * 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_for_nick(const char *nick, const char *data) {
+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;
-    char *encrypted;
+    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);
+    key = keystore_get_key(nick, &mode);
     if (!key) return NULL;
-    
-    /* Encrypt */
-    encrypted = fish_encrypt(key, strlen(key), data);
-    
-    g_free(key);
-    return encrypted;
+
+    *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.
+ * 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) {
+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;
 }
 
diff --git a/plugins/fishlim/fish.h b/plugins/fishlim/fish.h
index 238f52e7..6a2e911c 100644
--- a/plugins/fishlim/fish.h
+++ b/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
@@ -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
 
diff --git a/plugins/fishlim/fishlim.vcxproj b/plugins/fishlim/fishlim.vcxproj
index 43ae1df9..579c2436 100644
--- a/plugins/fishlim/fishlim.vcxproj
+++ b/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">

@@ -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" />

diff --git a/plugins/fishlim/fishlim.vcxproj.filters b/plugins/fishlim/fishlim.vcxproj.filters
index d8fbf454..ee4e855a 100644
--- a/plugins/fishlim/fishlim.vcxproj.filters
+++ b/plugins/fishlim/fishlim.vcxproj.filters
@@ -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>

diff --git a/plugins/fishlim/keystore.c b/plugins/fishlim/keystore.c
index 061faa03..adefc700 100644
--- a/plugins/fishlim/keystore.c
+++ b/plugins/fishlim/keystore.c
@@ -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);
diff --git a/plugins/fishlim/keystore.h b/plugins/fishlim/keystore.h
index 3d90606a..4af46693 100644
--- a/plugins/fishlim/keystore.h
+++ b/plugins/fishlim/keystore.h
@@ -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
diff --git a/plugins/fishlim/meson.build b/plugins/fishlim/meson.build
index 894923aa..65ccc9ea 100644
--- a/plugins/fishlim/meson.build
+++ b/plugins/fishlim/meson.build
@@ -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'
diff --git a/plugins/fishlim/plugin_hexchat.c b/plugins/fishlim/plugin_hexchat.c
index 3eaad986..83286e28 100644
--- a/plugins/fishlim/plugin_hexchat.c
+++ b/plugins/fishlim/plugin_hexchat.c
@@ -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>";
@@ -54,6 +55,13 @@ 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.
  */
 gchar *get_config_filename(void) {
@@ -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, &parameters_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,21 @@ 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 +442,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 +453,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 +472,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 +490,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 +512,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;
+
+    /* 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");
 
-    /* Check syntax */
-    if (*word[2] == '\0' || *word[3] != '\0') {
-        hexchat_printf(ph, "%s\n", usage_delkey);
-        return HEXCHAT_EAT_HEXCHAT;
+        /* 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 +554,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 +566,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 +583,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 +598,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 +676,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;
 }
 
 /**
diff --git a/plugins/fishlim/tests/fake/keystore.c b/plugins/fishlim/tests/fake/keystore.c
new file mode 100644
index 00000000..854f38dc
--- /dev/null
+++ b/plugins/fishlim/tests/fake/keystore.c
@@ -0,0 +1,47 @@
+/*
+
+  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;
+}
diff --git a/plugins/fishlim/tests/meson.build b/plugins/fishlim/tests/meson.build
new file mode 100644
index 00000000..3b75018e
--- /dev/null
+++ b/plugins/fishlim/tests/meson.build
@@ -0,0 +1,17 @@
+subdir('old_version')
+
+fishlim_test_sources = [
+  'tests.c',
+  'fake/keystore.c',
+  '../fish.c',
+  '../utils.c',
+]
+
+fishlim_tests = executable('fishlim_tests', fishlim_test_sources,
+  dependencies: [libgio_dep, libssl_dep],
+  link_with : fishlim_old_lib
+)
+
+test('Fishlim Tests', fishlim_tests,
+  timeout: 90
+)
diff --git a/plugins/fishlim/tests/old_version/fish.c b/plugins/fishlim/tests/old_version/fish.c
new file mode 100644
index 00000000..99601398
--- /dev/null
+++ b/plugins/fishlim/tests/old_version/fish.c
@@ -0,0 +1,155 @@
+/*
+
+  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.
+
+*/
+
+#ifdef __APPLE__
+#define __AVAILABILITYMACROS__
+#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <openssl/blowfish.h>
+
+#include "fish.h"
+
+#define IB 64
+static const char fish_base64[64] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+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,
+/*      !  "  #  $  %  &  '  (    )  *  +  ,  -  .  / */
+    IB,IB,IB,IB,IB,IB,IB,IB,  IB,IB,IB,IB,IB,IB, 0, 1,
+/*   0  1  2  3  4  5  6  7    8  9  :  ;  <  =  >  ? */
+     2, 3, 4, 5, 6, 7, 8, 9,  10,11,IB,IB,IB,IB,IB,IB,
+/*   @  A  B  C  D  E  F  G    H  I  J  K  L  M  N  O */
+    IB,38,39,40,41,42,43,44,  45,46,47,48,49,50,51,52,
+/*   P  Q  R  S  T  U  V  W    X  Y  Z  [  \  ]  ^  _*/
+    53,54,55,56,57,58,59,60,  61,62,63,IB,IB,IB,IB,IB,
+/*   `  a  b  c  d  e  f  g    h  i  j  k  l  m  n  o */
+    IB,12,13,14,15,16,17,18,  19,20,21,22,23,24,25,26,
+/*   p  q  r  s  t  u  v  w    x  y  z  {  |  }  ~  <del> */
+    27,28,29,30,31,32,33,34,  35,36,37,IB,IB,IB,IB,IB,
+};
+
+#define GET_BYTES(dest, source) do { \
+    *((dest)++) = ((source) >> 24) & 0xFF; \
+    *((dest)++) = ((source) >> 16) & 0xFF; \
+    *((dest)++) = ((source) >> 8) & 0xFF; \
+    *((dest)++) = (source) & 0xFF; \
+} while (0);
+
+
+char *__old_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;
+        }
+        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 *__old_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;
+}
\ No newline at end of file
diff --git a/plugins/fishlim/tests/old_version/fish.h b/plugins/fishlim/tests/old_version/fish.h
new file mode 100644
index 00000000..7037782b
--- /dev/null
+++ b/plugins/fishlim/tests/old_version/fish.h
@@ -0,0 +1,37 @@
+/*
+
+  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.
+
+*/
+
+#ifndef FISH_OLD_H
+#define FISH_OLD_H
+
+#include <stddef.h>
+
+#include <glib.h>
+
+char *__old_fish_encrypt(const char *key, size_t keylen, const char *message);
+char *__old_fish_decrypt(const char *key, size_t keylen, const char *data);
+
+#endif
+
+
diff --git a/plugins/fishlim/tests/old_version/meson.build b/plugins/fishlim/tests/old_version/meson.build
new file mode 100644
index 00000000..54647d71
--- /dev/null
+++ b/plugins/fishlim/tests/old_version/meson.build
@@ -0,0 +1,4 @@
+fishlim_old_lib = shared_library('fishlim_old_lib',
+  ['fish.c'],
+  dependencies: [libgio_dep, libssl_dep],
+)
diff --git a/plugins/fishlim/tests/tests.c b/plugins/fishlim/tests/tests.c
new file mode 100644
index 00000000..bb841c5e
--- /dev/null
+++ b/plugins/fishlim/tests/tests.c
@@ -0,0 +1,297 @@
+/*
+
+  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_TEST_H
+#define PLUGIN_HEXCHAT_FISHLIM_TEST_H
+
+// Libs
+#include <glib.h>
+// Project Libs
+#include "../fish.h"
+#include "../utils.h"
+#include "old_version/fish.h"
+
+/**
+ * Auxiliary function: Generate a random string
+ * @param out Preallocated string to fill
+ * @param len Size of bytes to fill
+ */
+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 and compare with old implementation
+ */
+void __ecb(void) {
+    char *bo64 = NULL, *b64 = NULL;
+    char *deo = NULL, *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 */
+            bo64 = __old_fish_encrypt(key, key_len, message);
+            g_assert_nonnull(bo64);
+            b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE);
+            g_assert_nonnull(b64);
+            g_assert_cmpuint(g_strcmp0(b64, bo64), == , 0);
+
+            /* Decrypt */
+            /* Linear */
+            deo = __old_fish_decrypt(key, key_len, bo64);
+            de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE);
+            g_assert_nonnull(deo);
+            g_assert_nonnull(de);
+            g_assert_cmpuint(g_strcmp0(de, message), == , 0);
+            g_assert_cmpuint(g_strcmp0(deo, message), == , 0);
+            g_assert_cmpuint(g_strcmp0(de, deo), == , 0);
+            g_free(deo);
+            g_free(de);
+            /* Mixed */
+            deo = __old_fish_decrypt(key, key_len, b64);
+            de = fish_decrypt_str(key, key_len, bo64, FISH_ECB_MODE);
+            g_assert_nonnull(deo);
+            g_assert_nonnull(de);
+            g_assert_cmpuint(g_strcmp0(de, message), == , 0);
+            g_assert_cmpuint(g_strcmp0(deo, message), == , 0);
+            g_assert_cmpuint(g_strcmp0(de, deo), == , 0);
+            g_free(deo);
+            g_free(de);
+
+            /* Free */
+            g_free(bo64);
+            g_free(b64);
+        }
+    }
+}
+
+/**
+ * Check encrypt and decrypt in CBC mode
+ */
+void __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_nonnull(de);
+            g_assert_cmpuint(g_strcmp0(de, message), == , 0);
+            g_free(de);
+
+            /* Free */
+            g_free(b64);
+        }
+    }
+}
+
+/**
+ * Check the calculation of final length from an encoded string in Base64
+ */
+void __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
+ */
+void __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
+ */
+void __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
+ */
+void __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
+ */
+void __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
+ */
+void __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", __ecb);
+    g_test_add_func("/fishlim/__cbc", __ecb);
+    g_test_add_func("/fishlim/__base64_len", __base64_len);
+    g_test_add_func("/fishlim/__base64_fish_len", __base64_fish_len);
+    g_test_add_func("/fishlim/__base64_ecb_len", __base64_ecb_len);
+    g_test_add_func("/fishlim/__base64_cbc_len", __base64_cbc_len);
+    g_test_add_func("/fishlim/__max_text_command_len", __max_text_command_len);
+    g_test_add_func("/fishlim/__foreach_utf8_data_chunks", __foreach_utf8_data_chunks);
+
+    return g_test_run();
+}
+
+#endif //PLUGIN_HEXCHAT_FISHLIM_TEST_H
\ No newline at end of file
diff --git a/plugins/fishlim/utils.c b/plugins/fishlim/utils.c
new file mode 100644
index 00000000..4052995a
--- /dev/null
+++ b/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;
+}
\ No newline at end of file
diff --git a/plugins/fishlim/utils.h b/plugins/fishlim/utils.h
new file mode 100644
index 00000000..623c67a4
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/plugins/lua/lua.c b/plugins/lua/lua.c
index 941342bb..d1370eaf 100644
--- a/plugins/lua/lua.c
+++ b/plugins/lua/lua.c
@@ -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);
@@ -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;
diff --git a/plugins/lua/lua.vcxproj b/plugins/lua/lua.vcxproj
index 22afe729..5c0be68e 100644
--- a/plugins/lua/lua.vcxproj
+++ b/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">
diff --git a/plugins/perl/perl.c b/plugins/perl/perl.c
index ab06dc96..6f30400c 100644
--- a/plugins/perl/perl.c
+++ b/plugins/perl/perl.c
@@ -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);
diff --git a/plugins/perl/perl.vcxproj b/plugins/perl/perl.vcxproj
index 8b5069c0..92c27408 100644
--- a/plugins/perl/perl.vcxproj
+++ b/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">

diff --git a/plugins/python/_hexchat.py b/plugins/python/_hexchat.py
new file mode 100644
index 00000000..5e4b0c65
--- /dev/null
+++ b/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)
diff --git a/plugins/python/generate_plugin.py b/plugins/python/generate_plugin.py
new file mode 100755
index 00000000..5c52b37b
--- /dev/null
+++ b/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])
diff --git a/plugins/python/hexchat.py b/plugins/python/hexchat.py
new file mode 100644
index 00000000..6922490b
--- /dev/null
+++ b/plugins/python/hexchat.py
@@ -0,0 +1 @@
+from _hexchat import *
diff --git a/plugins/python/meson.build b/plugins/python/meson.build
index e24f0c6f..5fd7ec2f 100644
--- a/plugins/python/meson.build
+++ b/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: '',
diff --git a/plugins/python/python.c b/plugins/python/python.c
deleted file mode 100644
index 4403474d..00000000
--- a/plugins/python/python.c
+++ /dev/null
@@ -1,2834 +0,0 @@
-/*
-* Copyright (c) 2002-2003  Gustavo Niemeyer <niemeyer@conectiva.com>
-*
-* XChat Python Plugin Interface
-*
-* Xchat Python Plugin Interface is free software; you can redistribute
-* it and/or modify it under the terms of the GNU General Public License
-* as published by the Free Software Foundation; either version 2 of the
-* License, or (at your option) any later version.
-*
-* pybot is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this file; if not, write to the Free Software
-* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
-*/
-
-/* Thread support
- * ==============
- *
- * The python interpreter has a global interpreter lock. Any thread
- * executing must acquire it before working with data accessible from
- * python code. Here we must also care about xchat not being
- * thread-safe. We do this by using an xchat lock, which protects
- * xchat instructions from being executed out of time (when this
- * plugin is not "active").
- *
- * When xchat calls python code:
- *   - Change the current_plugin for the executing plugin;
- *   - Release xchat lock
- *   - Acquire the global interpreter lock
- *   - Make the python call
- *   - Release the global interpreter lock
- *   - Acquire xchat lock
- *
- * When python code calls xchat:
- *   - Release the global interpreter lock
- *   - Acquire xchat lock
- *   - Restore context, if necessary
- *   - Make the xchat call
- *   - Release xchat lock
- *   - Acquire the global interpreter lock
- *
- * Inside a timer, so that individual threads have a chance to run:
- *   - Release xchat lock
- *   - Go ahead threads. Have a nice time!
- *   - Acquire xchat lock
- *
- */
-
-#include "config.h"
-
-#include <glib.h>
-#include <glib/gstdio.h>
-#include <string.h>
-#include <stdlib.h>
-#include <sys/types.h>
-
-#ifdef WIN32
-#include <direct.h>
-#else
-#include <unistd.h>
-#include <dirent.h>
-#endif
-
-#include "hexchat-plugin.h"
-#undef _POSIX_C_SOURCE	/* Avoid warnings from /usr/include/features.h */
-#undef _XOPEN_SOURCE
-#undef HAVE_MEMRCHR /* Avoid redefinition in Python.h */
-#undef HAVE_STRINGS_H
-#include <Python.h>
-#include <structmember.h>
-#include <pythread.h>
-
-/* Macros to convert version macros into string literals.
- * The indirect macro is a well-known preprocessor trick to force X to be evaluated before the # operator acts to make it a string literal.
- * If STRINGIZE were to be directly defined as #X instead, VERSION would be "VERSION_MAJOR" instead of "1".
- */
-#define STRINGIZE2(X) #X
-#define STRINGIZE(X) STRINGIZE2(X)
-
-/* Version number macros */
-#define VERSION_MAJOR 1
-#define VERSION_MINOR 0
-
-/* Version string macro e.g 1.0/3.3 */
-#define VERSION STRINGIZE(VERSION_MAJOR) "." STRINGIZE(VERSION_MINOR) "/" \
-				STRINGIZE(PY_MAJOR_VERSION) "." STRINGIZE (PY_MINOR_VERSION)
-
-/* #define's for Python 2 */
-#if PY_MAJOR_VERSION == 2
-#undef PyLong_Check
-#define PyLong_Check PyInt_Check
-#define PyLong_AsLong PyInt_AsLong
-#define PyLong_FromLong PyInt_FromLong
-
-#undef PyUnicode_Check
-#undef PyUnicode_FromString
-#undef PyUnicode_FromFormat
-#define PyUnicode_Check PyString_Check
-#define PyUnicode_AsFormat PyString_AsFormat
-#define PyUnicode_FromFormat PyString_FromFormat
-#define PyUnicode_FromString PyString_FromString
-#define PyUnicode_AsUTF8 PyString_AsString
-
-#ifdef WIN32
-#undef WITH_THREAD
-#endif
-#endif
-
-/* #define for Python 3 */
-#if PY_MAJOR_VERSION == 3
-#define IS_PY3K
-#endif
-
-#define NONE 0
-#define ALLOW_THREADS 1
-#define RESTORE_CONTEXT 2
-
-#ifdef WITH_THREAD
-#define ACQUIRE_XCHAT_LOCK() PyThread_acquire_lock(xchat_lock, 1)
-#define RELEASE_XCHAT_LOCK() PyThread_release_lock(xchat_lock)
-#define BEGIN_XCHAT_CALLS(x) \
-	do { \
-		PyObject *calls_plugin = NULL; \
-		PyThreadState *calls_thread; \
-		if ((x) & RESTORE_CONTEXT) \
-			calls_plugin = Plugin_GetCurrent(); \
-		calls_thread = PyEval_SaveThread(); \
-		ACQUIRE_XCHAT_LOCK(); \
-		if (!((x) & ALLOW_THREADS)) { \
-			PyEval_RestoreThread(calls_thread); \
-			calls_thread = NULL; \
-		} \
-		if (calls_plugin) \
-			hexchat_set_context(ph, \
-				Plugin_GetContext(calls_plugin)); \
-		while (0)
-#define END_XCHAT_CALLS() \
-		RELEASE_XCHAT_LOCK(); \
-		if (calls_thread) \
-			PyEval_RestoreThread(calls_thread); \
-	} while(0)
-#else
-#define ACQUIRE_XCHAT_LOCK()
-#define RELEASE_XCHAT_LOCK()
-#define BEGIN_XCHAT_CALLS(x)
-#define END_XCHAT_CALLS()
-#endif
-
-#ifdef WITH_THREAD
-
-#define BEGIN_PLUGIN(plg) \
-	do { \
-	hexchat_context *begin_plugin_ctx = hexchat_get_context(ph); \
-	RELEASE_XCHAT_LOCK(); \
-	Plugin_AcquireThread(plg); \
-	Plugin_SetContext(plg, begin_plugin_ctx); \
-	} while (0)
-#define END_PLUGIN(plg) \
-	do { \
-	Plugin_ReleaseThread(plg); \
-	ACQUIRE_XCHAT_LOCK(); \
-	} while (0)
-
-#else /* !WITH_THREAD (win32) */
-
-static PyThreadState *pTempThread;
-
-#define BEGIN_PLUGIN(plg) \
-	do { \
-	hexchat_context *begin_plugin_ctx = hexchat_get_context(ph); \
-	RELEASE_XCHAT_LOCK(); \
-	PyEval_AcquireLock(); \
-	pTempThread = PyThreadState_Swap(((PluginObject *)(plg))->tstate); \
-	Plugin_SetContext(plg, begin_plugin_ctx); \
-	} while (0)
-#define END_PLUGIN(plg) \
-	do { \
-	((PluginObject *)(plg))->tstate = PyThreadState_Swap(pTempThread); \
-	PyEval_ReleaseLock(); \
-	ACQUIRE_XCHAT_LOCK(); \
-	} while (0)
-
-#endif /* !WITH_THREAD */
-
-#define Plugin_Swap(x) \
-	PyThreadState_Swap(((PluginObject *)(x))->tstate)
-#define Plugin_AcquireThread(x) \
-	PyEval_AcquireThread(((PluginObject *)(x))->tstate)
-#define Plugin_ReleaseThread(x) \
-	Util_ReleaseThread(((PluginObject *)(x))->tstate)
-#define Plugin_GetFilename(x) \
-	(((PluginObject *)(x))->filename)
-#define Plugin_GetName(x) \
-	(((PluginObject *)(x))->name)
-#define Plugin_GetVersion(x) \
-	(((PluginObject *)(x))->version)
-#define Plugin_GetDesc(x) \
-	(((PluginObject *)(x))->description)
-#define Plugin_GetHooks(x) \
-	(((PluginObject *)(x))->hooks)
-#define Plugin_GetContext(x) \
-	(((PluginObject *)(x))->context)
-#define Plugin_SetFilename(x, y) \
-	((PluginObject *)(x))->filename = (y);
-#define Plugin_SetName(x, y) \
-	((PluginObject *)(x))->name = (y);
-#define Plugin_SetVersion(x, y) \
-	((PluginObject *)(x))->version = (y);
-#define Plugin_SetDescription(x, y) \
-	((PluginObject *)(x))->description = (y);
-#define Plugin_SetHooks(x, y) \
-	((PluginObject *)(x))->hooks = (y);
-#define Plugin_SetContext(x, y) \
-	((PluginObject *)(x))->context = (y);
-#define Plugin_SetGui(x, y) \
-	((PluginObject *)(x))->gui = (y);
-
-#define HOOK_XCHAT  1
-#define HOOK_XCHAT_ATTR 2
-#define HOOK_UNLOAD 3
-
-/* ===================================================================== */
-/* Object definitions */
-
-typedef struct {
-	PyObject_HEAD
-	int softspace; /* We need it for print support. */
-} XChatOutObject;
-
-typedef struct {
-	PyObject_HEAD
-	hexchat_context *context;
-} ContextObject;
-
-typedef struct {
-	PyObject_HEAD
-	PyObject *time;
-} AttributeObject;
-
-typedef struct {
-	PyObject_HEAD
-	const char *listname;
-	PyObject *dict;
-} ListItemObject;
-
-typedef struct {
-	PyObject_HEAD
-	char *name;
-	char *version;
-	char *filename;
-	char *description;
-	GSList *hooks;
-	PyThreadState *tstate;
-	hexchat_context *context;
-	void *gui;
-} PluginObject;
-
-typedef struct {
-	int type;
-	PyObject *plugin;
-	PyObject *callback;
-	PyObject *userdata;
-	char *name;
-	void *data; /* A handle, when type == HOOK_XCHAT */
-} Hook;
-
-
-/* ===================================================================== */
-/* Function declarations */
-
-static PyObject *Util_BuildList(char *word[]);
-static PyObject *Util_BuildEOLList(char *word[]);
-static void Util_Autoload(void);
-static char *Util_Expand(char *filename);
-
-static int Callback_Server(char *word[], char *word_eol[], hexchat_event_attrs *attrs, void *userdata);
-static int Callback_Command(char *word[], char *word_eol[], void *userdata);
-static int Callback_Print_Attrs(char *word[], hexchat_event_attrs *attrs, void *userdata);
-static int Callback_Print(char *word[], void *userdata);
-static int Callback_Timer(void *userdata);
-static int Callback_ThreadTimer(void *userdata);
-
-static PyObject *XChatOut_New(void);
-static PyObject *XChatOut_write(PyObject *self, PyObject *args);
-static void XChatOut_dealloc(PyObject *self);
-
-static PyObject *Attribute_New(hexchat_event_attrs *attrs);
-
-static void Context_dealloc(PyObject *self);
-static PyObject *Context_set(ContextObject *self, PyObject *args);
-static PyObject *Context_command(ContextObject *self, PyObject *args);
-static PyObject *Context_prnt(ContextObject *self, PyObject *args);
-static PyObject *Context_get_info(ContextObject *self, PyObject *args);
-static PyObject *Context_get_list(ContextObject *self, PyObject *args);
-static PyObject *Context_compare(ContextObject *a, ContextObject *b, int op);
-static PyObject *Context_FromContext(hexchat_context *context);
-static PyObject *Context_FromServerAndChannel(char *server, char *channel);
-
-static PyObject *Plugin_New(char *filename, PyObject *xcoobj);
-static PyObject *Plugin_GetCurrent(void);
-static PluginObject *Plugin_ByString(char *str);
-static Hook *Plugin_AddHook(int type, PyObject *plugin, PyObject *callback,
-			    PyObject *userdata, char *name, void *data);
-static Hook *Plugin_FindHook(PyObject *plugin, char *name);
-static void Plugin_RemoveHook(PyObject *plugin, Hook *hook);
-static void Plugin_RemoveAllHooks(PyObject *plugin);
-
-static PyObject *Module_hexchat_command(PyObject *self, PyObject *args);
-static PyObject *Module_xchat_prnt(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_get_context(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_find_context(PyObject *self, PyObject *args,
-					   PyObject *kwargs);
-static PyObject *Module_hexchat_get_info(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_hook_command(PyObject *self, PyObject *args,
-					   PyObject *kwargs);
-static PyObject *Module_hexchat_hook_server(PyObject *self, PyObject *args,
-					  PyObject *kwargs);
-static PyObject *Module_hexchat_hook_print(PyObject *self, PyObject *args,
-					 PyObject *kwargs);
-static PyObject *Module_hexchat_hook_timer(PyObject *self, PyObject *args,
-					 PyObject *kwargs);
-static PyObject *Module_hexchat_unhook(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_get_info(PyObject *self, PyObject *args);
-static PyObject *Module_xchat_get_list(PyObject *self, PyObject *args);
-static PyObject *Module_xchat_get_lists(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_nickcmp(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_strip(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_pluginpref_set(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_pluginpref_get(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_pluginpref_delete(PyObject *self, PyObject *args);
-static PyObject *Module_hexchat_pluginpref_list(PyObject *self, PyObject *args);
-
-static void IInterp_Exec(char *command);
-static int IInterp_Cmd(char *word[], char *word_eol[], void *userdata);
-
-static void Command_PyList(void);
-static void Command_PyLoad(char *filename);
-static void Command_PyUnload(char *name);
-static void Command_PyReload(char *name);
-static void Command_PyAbout(void);
-static int Command_Py(char *word[], char *word_eol[], void *userdata);
-
-/* ===================================================================== */
-/* Static declarations and definitions */
-
-static PyTypeObject Plugin_Type;
-static PyTypeObject XChatOut_Type;
-static PyTypeObject Context_Type;
-static PyTypeObject ListItem_Type;
-static PyTypeObject Attribute_Type;
-
-static PyThreadState *main_tstate = NULL;
-static void *thread_timer = NULL;
-
-static hexchat_plugin *ph;
-static GSList *plugin_list = NULL;
-
-static PyObject *interp_plugin = NULL;
-static PyObject *xchatout = NULL;
-
-#ifdef WITH_THREAD
-static PyThread_type_lock xchat_lock = NULL;
-#endif
-
-static const char usage[] = "\
-Usage: /PY LOAD   <filename>\n\
-           UNLOAD <filename|name>\n\
-           RELOAD <filename|name>\n\
-           LIST\n\
-           EXEC <command>\n\
-           CONSOLE\n\
-           ABOUT\n\
-\n";
-
-static const char about[] = "HexChat Python interface version " VERSION "\n";
-
-/* ===================================================================== */
-/* Utility functions */
-
-static PyObject *
-Util_BuildList(char *word[])
-{
-	PyObject *list;
-	int listsize = 31;
-	int i;
-	/* Find the last valid array member; there may be intermediate NULLs that
-	 * would otherwise cause us to drop some members. */
-	while (listsize > 0 &&
-	       (word[listsize] == NULL || word[listsize][0] == 0))
-		listsize--;
-	list = PyList_New(listsize);
-	if (list == NULL) {
-		PyErr_Print();
-		return NULL;
-	}
-	for (i = 1; i <= listsize; i++) {
-		PyObject *o;
-		if (word[i] == NULL) {
-			Py_INCREF(Py_None);
-			o = Py_None;
-		} else {
-			/* This handles word[i][0] == 0 automatically. */
-			o = PyUnicode_FromString(word[i]);
-		}
-		PyList_SetItem(list, i - 1, o);
-	}
-	return list;
-}
-
-static PyObject *
-Util_BuildEOLList(char *word[])
-{
-	PyObject *list;
-	int listsize = 31;
-	int i;
-	char *accum = NULL;
-	char *last = NULL;
-
-	/* Find the last valid array member; there may be intermediate NULLs that
-	 * would otherwise cause us to drop some members. */
-	while (listsize > 0 &&
-	       (word[listsize] == NULL || word[listsize][0] == 0))
-		listsize--;
-	list = PyList_New(listsize);
-	if (list == NULL) {
-		PyErr_Print();
-		return NULL;
-	}
-	for (i = listsize; i > 0; i--) {
-		char *part = word[i];
-		PyObject *uni_part;
-		if (accum == NULL) {
-			accum = g_strdup (part);
-		} else if (part != NULL && part[0] != 0) {
-			last = accum;
-			accum = g_strjoin(" ", part, last, NULL);
-			g_free (last);
-			last = NULL;
-
-			if (accum == NULL) {
-				Py_DECREF(list);
-				hexchat_print(ph, "Not enough memory to alloc accum"
-				              "for python plugin callback");
-				return NULL;
-			}
-		}
-		uni_part = PyUnicode_FromString(accum);
-		PyList_SetItem(list, i - 1, uni_part);
-	}
-
-	g_free (last);
-	g_free (accum);
-
-	return list;
-}
-
-static void
-Util_Autoload_from (const char *dir_name)
-{
-	gchar *oldcwd;
-	const char *entry_name;
-	GDir *dir;
-
-	oldcwd = g_get_current_dir ();
-	if (oldcwd == NULL)
-		return;
-	if (g_chdir(dir_name) != 0)
-	{
-		g_free (oldcwd);
-		return;
-	}
-	dir = g_dir_open (".", 0, NULL);
-	if (dir == NULL)
-	{
-		g_free (oldcwd);
-		return;
-	}
-	while ((entry_name = g_dir_read_name (dir)))
-	{
-		if (g_str_has_suffix (entry_name, ".py"))
-			Command_PyLoad((char*)entry_name);
-	}
-	g_dir_close (dir);
-	g_chdir (oldcwd);
-}
-
-static void
-Util_Autoload()
-{
-	const char *xdir;
-	char *sub_dir;
-	/* we need local filesystem encoding for g_chdir, g_dir_open etc */
-
-	xdir = hexchat_get_info(ph, "configdir");
-
-	/* auto-load from subdirectory addons */
-	sub_dir = g_build_filename (xdir, "addons", NULL);
-	Util_Autoload_from(sub_dir);
-	g_free (sub_dir);
-}
-
-static char *
-Util_Expand(char *filename)
-{
-	char *expanded;
-
-	/* Check if this is an absolute path. */
-	if (g_path_is_absolute(filename)) {
-		if (g_file_test(filename, G_FILE_TEST_EXISTS))
-			return g_strdup(filename);
-		else
-			return NULL;
-	}
-
-	/* Check if it starts with ~/ and expand the home if positive. */
-	if (*filename == '~' && *(filename+1) == '/') {
-		expanded = g_build_filename(g_get_home_dir(),
-					    filename+2, NULL);
-		if (g_file_test(expanded, G_FILE_TEST_EXISTS))
-			return expanded;
-		else {
-			g_free(expanded);
-			return NULL;
-		}
-	}
-
-	/* Check if it's in the current directory. */
-	expanded = g_build_filename(g_get_current_dir(),
-				    filename, NULL);
-	if (g_file_test(expanded, G_FILE_TEST_EXISTS))
-		return expanded;
-	g_free(expanded);
-
-	/* Check if ~/.config/hexchat/addons/<filename> exists. */
-	expanded = g_build_filename(hexchat_get_info(ph, "configdir"),
-				    "addons", filename, NULL);
-	if (g_file_test(expanded, G_FILE_TEST_EXISTS))
-		return expanded;
-	g_free(expanded);
-
-	return NULL;
-}
-
-/* Similar to PyEval_ReleaseThread, but accepts NULL thread states. */
-static void
-Util_ReleaseThread(PyThreadState *tstate)
-{
-	PyThreadState *old_tstate;
-	if (tstate == NULL)
-		Py_FatalError("PyEval_ReleaseThread: NULL thread state");
-	old_tstate = PyThreadState_Swap(NULL);
-	if (old_tstate != tstate && old_tstate != NULL)
-		Py_FatalError("PyEval_ReleaseThread: wrong thread state");
-	PyEval_ReleaseLock();
-}
-
-/* ===================================================================== */
-/* Hookable functions. These are the entry points to python code, besides
- * the load function, and the hooks for interactive interpreter. */
-
-static int
-Callback_Server(char *word[], char *word_eol[], hexchat_event_attrs *attrs, void *userdata)
-{
-	Hook *hook = (Hook *) userdata;
-	PyObject *retobj;
-	PyObject *word_list, *word_eol_list;
-	PyObject *attributes;
-	int ret = HEXCHAT_EAT_NONE;
-	PyObject *plugin;
-
-	plugin = hook->plugin;
-	BEGIN_PLUGIN(plugin);
-
-	word_list = Util_BuildList(word);
-	if (word_list == NULL) {
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-	word_eol_list = Util_BuildList(word_eol);
-	if (word_eol_list == NULL) {
-		Py_DECREF(word_list);
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-
-	attributes = Attribute_New(attrs);
-
-	if (hook->type == HOOK_XCHAT_ATTR)
-		retobj = PyObject_CallFunction(hook->callback, "(OOOO)", word_list,
-					       word_eol_list, hook->userdata, attributes);
-	else
-		retobj = PyObject_CallFunction(hook->callback, "(OOO)", word_list,
-					       word_eol_list, hook->userdata);
-	Py_DECREF(word_list);
-	Py_DECREF(word_eol_list);
-	Py_DECREF(attributes);
-
-	if (retobj == Py_None) {
-		ret = HEXCHAT_EAT_NONE;
-		Py_DECREF(retobj);
-	} else if (retobj) {
-		ret = PyLong_AsLong(retobj);
-		Py_DECREF(retobj);
-	} else {
-		PyErr_Print();
-	}
-
-	END_PLUGIN(plugin);
-
-	return ret;
-}
-
-static int
-Callback_Command(char *word[], char *word_eol[], void *userdata)
-{
-	Hook *hook = (Hook *) userdata;
-	PyObject *retobj;
-	PyObject *word_list, *word_eol_list;
-	int ret = HEXCHAT_EAT_NONE;
-	PyObject *plugin;
-
-	plugin = hook->plugin;
-	BEGIN_PLUGIN(plugin);
-
-	word_list = Util_BuildList(word);
-	if (word_list == NULL) {
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-	word_eol_list = Util_BuildList(word_eol);
-	if (word_eol_list == NULL) {
-		Py_DECREF(word_list);
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-
-	retobj = PyObject_CallFunction(hook->callback, "(OOO)", word_list,
-				       word_eol_list, hook->userdata);
-	Py_DECREF(word_list);
-	Py_DECREF(word_eol_list);
-
-	if (retobj == Py_None) {
-		ret = HEXCHAT_EAT_NONE;
-		Py_DECREF(retobj);
-	} else if (retobj) {
-		ret = PyLong_AsLong(retobj);
-		Py_DECREF(retobj);
-	} else {
-		PyErr_Print();
-	}
-
-	END_PLUGIN(plugin);
-
-	return ret;
-}
-
-static int
-Callback_Print_Attrs(char *word[], hexchat_event_attrs *attrs, void *userdata)
-{
-	Hook *hook = (Hook *) userdata;
-	PyObject *retobj;
-	PyObject *word_list;
-	PyObject *word_eol_list;
-	PyObject *attributes;
-	int ret = HEXCHAT_EAT_NONE;
-	PyObject *plugin;
-
-	plugin = hook->plugin;
-	BEGIN_PLUGIN(plugin);
-
-	word_list = Util_BuildList(word);
-	if (word_list == NULL) {
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-	word_eol_list = Util_BuildEOLList(word);
-	if (word_eol_list == NULL) {
-		Py_DECREF(word_list);
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-
-	attributes = Attribute_New(attrs);
-
-	retobj = PyObject_CallFunction(hook->callback, "(OOOO)", word_list,
-					    word_eol_list, hook->userdata, attributes);
-
-	Py_DECREF(word_list);
-	Py_DECREF(word_eol_list);
-	Py_DECREF(attributes);
-
-	if (retobj == Py_None) {
-		ret = HEXCHAT_EAT_NONE;
-		Py_DECREF(retobj);
-	} else if (retobj) {
-		ret = PyLong_AsLong(retobj);
-		Py_DECREF(retobj);
-	} else {
-		PyErr_Print();
-	}
-
-	END_PLUGIN(plugin);
-
-	return ret;
-}
-
-static int
-Callback_Print(char *word[], void *userdata)
-{
-	Hook *hook = (Hook *) userdata;
-	PyObject *retobj;
-	PyObject *word_list;
-	PyObject *word_eol_list;
-	int ret = HEXCHAT_EAT_NONE;
-	PyObject *plugin;
-
-	plugin = hook->plugin;
-	BEGIN_PLUGIN(plugin);
-
-	word_list = Util_BuildList(word);
-	if (word_list == NULL) {
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-	word_eol_list = Util_BuildEOLList(word);
-	if (word_eol_list == NULL) {
-		Py_DECREF(word_list);
-		END_PLUGIN(plugin);
-		return HEXCHAT_EAT_NONE;
-	}
-
-	retobj = PyObject_CallFunction(hook->callback, "(OOO)", word_list,
-					       word_eol_list, hook->userdata);
-
-	Py_DECREF(word_list);
-	Py_DECREF(word_eol_list);
-
-	if (retobj == Py_None) {
-		ret = HEXCHAT_EAT_NONE;
-		Py_DECREF(retobj);
-	} else if (retobj) {
-		ret = PyLong_AsLong(retobj);
-		Py_DECREF(retobj);
-	} else {
-		PyErr_Print();
-	}
-
-	END_PLUGIN(plugin);
-
-	return ret;
-}
-
-static int
-Callback_Timer(void *userdata)
-{
-	Hook *hook = (Hook *) userdata;
-	PyObject *retobj;
-	int ret = 0;
-	PyObject *plugin;
-
-	plugin = hook->plugin;
-
-	BEGIN_PLUGIN(hook->plugin);
-
-	retobj = PyObject_CallFunction(hook->callback, "(O)", hook->userdata);
-
-	if (retobj) {
-		ret = PyObject_IsTrue(retobj);
-		Py_DECREF(retobj);
-	} else {
-		PyErr_Print();
-	}
-
-	/* Returning 0 for this callback unhooks itself. */
-	if (ret == 0)
-		Plugin_RemoveHook(plugin, hook);
-
-	END_PLUGIN(plugin);
-
-	return ret;
-}
-
-#ifdef WITH_THREAD
-static int
-Callback_ThreadTimer(void *userdata)
-{
-	RELEASE_XCHAT_LOCK();
-#ifndef WIN32
-	usleep(1);
-#endif
-	ACQUIRE_XCHAT_LOCK();
-	return 1;
-}
-#endif
-
-/* ===================================================================== */
-/* XChatOut object */
-
-/* We keep this information global, so we can reset it when the
- * deinit function is called. */
-/* XXX This should be somehow bound to the printing context. */
-static GString *xchatout_buffer = NULL;
-
-static PyObject *
-XChatOut_New()
-{
-	XChatOutObject *xcoobj;
-	xcoobj = PyObject_New(XChatOutObject, &XChatOut_Type);
-	if (xcoobj != NULL)
-		xcoobj->softspace = 0;
-	return (PyObject *) xcoobj;
-}
-
-static void
-XChatOut_dealloc(PyObject *self)
-{
-	Py_TYPE(self)->tp_free((PyObject *)self);
-}
-
-/* This is a little bit complex because we have to buffer data
- * until a \n is received, since xchat breaks the line automatically.
- * We also crop the last \n for this reason. */
-static PyObject *
-XChatOut_write(PyObject *self, PyObject *args)
-{
-	gboolean add_space;
-	char *data, *pos;
-
-	if (!PyArg_ParseTuple(args, "s:write", &data))
-		return NULL;
-	if (!data || !*data) {
-		Py_RETURN_NONE;
-	}
-	BEGIN_XCHAT_CALLS(RESTORE_CONTEXT|ALLOW_THREADS);
-	if (((XChatOutObject *)self)->softspace) {
-		add_space = TRUE;
-		((XChatOutObject *)self)->softspace = 0;
-	} else {
-		add_space = FALSE;
-	}
-
-	g_string_append (xchatout_buffer, data);
-
-	/* If not end of line add space to continue buffer later */
-	if (add_space && xchatout_buffer->str[xchatout_buffer->len - 1] != '\n')
-	{
-		g_string_append_c (xchatout_buffer, ' ');
-	}
-
-	/* If there is an end of line print up to that */
-	if ((pos = strrchr (xchatout_buffer->str, '\n')))
-	{
-		*pos = '\0';
-		hexchat_print (ph, xchatout_buffer->str);
-
-		/* Then remove it from buffer */
-		g_string_erase (xchatout_buffer, 0, pos - xchatout_buffer->str + 1);
-	}
-
-	END_XCHAT_CALLS();
-	Py_RETURN_NONE;
-}
-
-#define OFF(x) offsetof(XChatOutObject, x)
-
-static PyMemberDef XChatOut_members[] = {
-	{"softspace", T_INT, OFF(softspace), 0},
-	{0}
-};
-
-static PyMethodDef XChatOut_methods[] = {
-	{"write", XChatOut_write, METH_VARARGS},
-	{NULL, NULL}
-};
-
-static PyTypeObject XChatOut_Type = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	"hexchat.XChatOut",	/*tp_name*/
-	sizeof(XChatOutObject),	/*tp_basicsize*/
-	0,			/*tp_itemsize*/
-	XChatOut_dealloc,	/*tp_dealloc*/
-	0,			/*tp_print*/
-	0,			/*tp_getattr*/
-	0,			/*tp_setattr*/
-	0,			/*tp_compare*/
-	0,			/*tp_repr*/
-	0,			/*tp_as_number*/
-	0,			/*tp_as_sequence*/
-	0,			/*tp_as_mapping*/
-	0,			/*tp_hash*/
-        0,                      /*tp_call*/
-        0,                      /*tp_str*/
-        PyObject_GenericGetAttr,/*tp_getattro*/
-        PyObject_GenericSetAttr,/*tp_setattro*/
-        0,                      /*tp_as_buffer*/
-        Py_TPFLAGS_DEFAULT,     /*tp_flags*/
-        0,                      /*tp_doc*/
-        0,                      /*tp_traverse*/
-        0,                      /*tp_clear*/
-        0,                      /*tp_richcompare*/
-        0,                      /*tp_weaklistoffset*/
-        0,                      /*tp_iter*/
-        0,                      /*tp_iternext*/
-        XChatOut_methods,       /*tp_methods*/
-        XChatOut_members,       /*tp_members*/
-        0,                      /*tp_getset*/
-        0,                      /*tp_base*/
-        0,                      /*tp_dict*/
-        0,                      /*tp_descr_get*/
-        0,                      /*tp_descr_set*/
-        0,                      /*tp_dictoffset*/
-        0,                      /*tp_init*/
-        PyType_GenericAlloc,    /*tp_alloc*/
-        PyType_GenericNew,      /*tp_new*/
-      	PyObject_Del,          /*tp_free*/
-        0,                      /*tp_is_gc*/
-};
-
-
-/* ===================================================================== */
-/* Attribute object */
-
-#undef OFF
-#define OFF(x) offsetof(AttributeObject, x)
-
-static PyMemberDef Attribute_members[] = {
-	{"time", T_OBJECT, OFF(time), 0},
-	{0}
-};
-
-static void
-Attribute_dealloc(PyObject *self)
-{
-	Py_DECREF(((AttributeObject*)self)->time);
-	Py_TYPE(self)->tp_free((PyObject *)self);
-}
-
-static PyObject *
-Attribute_repr(PyObject *self)
-{
-	return PyUnicode_FromFormat("<Attribute object at %p>", self);
-}
-
-static PyTypeObject Attribute_Type = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	"hexchat.Attribute",	/*tp_name*/
-	sizeof(AttributeObject),	/*tp_basicsize*/
-	0,			/*tp_itemsize*/
-	Attribute_dealloc,	/*tp_dealloc*/
-	0,			/*tp_print*/
-	0,			/*tp_getattr*/
-	0,			/*tp_setattr*/
-	0,			/*tp_compare*/
-	Attribute_repr,		/*tp_repr*/
-	0,			/*tp_as_number*/
-	0,			/*tp_as_sequence*/
-	0,			/*tp_as_mapping*/
-	0,			/*tp_hash*/
-        0,                      /*tp_call*/
-        0,                      /*tp_str*/
-        PyObject_GenericGetAttr,/*tp_getattro*/
-        PyObject_GenericSetAttr,/*tp_setattro*/
-        0,                      /*tp_as_buffer*/
-        Py_TPFLAGS_DEFAULT,     /*tp_flags*/
-        0,                      /*tp_doc*/
-        0,                      /*tp_traverse*/
-        0,                      /*tp_clear*/
-        0,                      /*tp_richcompare*/
-        0,                      /*tp_weaklistoffset*/
-        0,                      /*tp_iter*/
-        0,                      /*tp_iternext*/
-        0,                      /*tp_methods*/
-        Attribute_members,		/*tp_members*/
-        0,                      /*tp_getset*/
-        0,                      /*tp_base*/
-        0,                      /*tp_dict*/
-        0,                      /*tp_descr_get*/
-        0,                      /*tp_descr_set*/
-        0,						/*tp_dictoffset*/
-        0,                      /*tp_init*/
-        PyType_GenericAlloc,    /*tp_alloc*/
-        PyType_GenericNew,      /*tp_new*/
-      	PyObject_Del,          /*tp_free*/
-        0,                      /*tp_is_gc*/
-};
-
-static PyObject *
-Attribute_New(hexchat_event_attrs *attrs)
-{
-	AttributeObject *attr;
-	attr = PyObject_New(AttributeObject, &Attribute_Type);
-	if (attr != NULL) {
-		attr->time = PyLong_FromLong((long)attrs->server_time_utc);
-	}
-	return (PyObject *) attr;
-}
-
-
-/* ===================================================================== */
-/* Context object */
-
-static void
-Context_dealloc(PyObject *self)
-{
-	Py_TYPE(self)->tp_free((PyObject *)self);
-}
-
-static PyObject *
-Context_set(ContextObject *self, PyObject *args)
-{
-	PyObject *plugin = Plugin_GetCurrent();
-	Plugin_SetContext(plugin, self->context);
-	Py_RETURN_NONE;
-}
-
-static PyObject *
-Context_command(ContextObject *self, PyObject *args)
-{
-	char *text;
-	if (!PyArg_ParseTuple(args, "s:command", &text))
-		return NULL;
-	BEGIN_XCHAT_CALLS(ALLOW_THREADS);
-	hexchat_set_context(ph, self->context);
-	hexchat_command(ph, text);
-	END_XCHAT_CALLS();
-	Py_RETURN_NONE;
-}
-
-static PyObject *
-Context_prnt(ContextObject *self, PyObject *args)
-{
-	char *text;
-	if (!PyArg_ParseTuple(args, "s:prnt", &text))
-		return NULL;
-	BEGIN_XCHAT_CALLS(ALLOW_THREADS);
-	hexchat_set_context(ph, self->context);
-	hexchat_print(ph, text);
-	END_XCHAT_CALLS();
-	Py_RETURN_NONE;
-}
-
-static PyObject *
-Context_emit_print(ContextObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *argv[6];
-	char *name;
-	int res;
-	long time = 0;
-	hexchat_event_attrs *attrs;
-	char *kwlist[] = {"name", "arg1", "arg2", "arg3",
-					"arg4", "arg5", "arg6", 
-					"time", NULL};
-	memset(&argv, 0, sizeof(char*)*6);
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|ssssssl:print_event", kwlist, &name,
-			      &argv[0], &argv[1], &argv[2],
-			      &argv[3], &argv[4], &argv[5],
-				  &time))
-		return NULL;
-	BEGIN_XCHAT_CALLS(ALLOW_THREADS);
-	hexchat_set_context(ph, self->context);
-	attrs = hexchat_event_attrs_create(ph);
-	attrs->server_time_utc = (time_t)time; 
-	
-	res = hexchat_emit_print_attrs(ph, attrs, name, argv[0], argv[1], argv[2],
-					 argv[3], argv[4], argv[5], NULL);
-
-	hexchat_event_attrs_free(ph, attrs);
-	END_XCHAT_CALLS();
-	return PyLong_FromLong(res);
-}
-
-static PyObject *
-Context_get_info(ContextObject *self, PyObject *args)
-{
-	const char *info;
-	char *name;
-	if (!PyArg_ParseTuple(args, "s:get_info", &name))
-		return NULL;
-	BEGIN_XCHAT_CALLS(NONE);
-	hexchat_set_context(ph, self->context);
-	info = hexchat_get_info(ph, name);
-	END_XCHAT_CALLS();
-	if (info == NULL) {
-		Py_RETURN_NONE;
-	}
-	return PyUnicode_FromString(info);
-}
-
-static PyObject *
-Context_get_list(ContextObject *self, PyObject *args)
-{
-	PyObject *plugin = Plugin_GetCurrent();
-	hexchat_context *saved_context = Plugin_GetContext(plugin);
-	PyObject *ret;
-	Plugin_SetContext(plugin, self->context);
-	ret = Module_xchat_get_list((PyObject*)self, args);
-	Plugin_SetContext(plugin, saved_context);
-	return ret;
-}
-
-/* needed to make context1 == context2 work */
-static PyObject *
-Context_compare(ContextObject *a, ContextObject *b, int op)
-{
-	PyObject *ret;
-	/* check for == */
-	if (op == Py_EQ)
-		ret = (a->context == b->context ? Py_True : Py_False);
-	/* check for != */
-	else if (op == Py_NE)
-		ret = (a->context != b->context ? Py_True : Py_False);
-	/* only makes sense as == and != */
-	else
-	{
-		PyErr_SetString(PyExc_TypeError, "contexts are either equal or not equal");
-		ret = Py_None;
-	}
-
-	Py_INCREF(ret);
-	return ret;
-}
-
-static PyMethodDef Context_methods[] = {
-	{"set", (PyCFunction) Context_set, METH_NOARGS},
-	{"command", (PyCFunction) Context_command, METH_VARARGS},
-	{"prnt", (PyCFunction) Context_prnt, METH_VARARGS},
-	{"emit_print", (PyCFunction) Context_emit_print, METH_VARARGS|METH_KEYWORDS},
-	{"get_info", (PyCFunction) Context_get_info, METH_VARARGS},
-	{"get_list", (PyCFunction) Context_get_list, METH_VARARGS},
-	{NULL, NULL}
-};
-
-static PyTypeObject Context_Type = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	"hexchat.Context",	/*tp_name*/
-	sizeof(ContextObject),	/*tp_basicsize*/
-	0,			/*tp_itemsize*/
-	Context_dealloc,        /*tp_dealloc*/
-	0,			/*tp_print*/
-	0,			/*tp_getattr*/
-	0,			/*tp_setattr*/
-	0,			/*tp_compare*/
-	0,			/*tp_repr*/
-	0,			/*tp_as_number*/
-	0,			/*tp_as_sequence*/
-	0,			/*tp_as_mapping*/
-	0,			/*tp_hash*/
-        0,                      /*tp_call*/
-        0,                      /*tp_str*/
-        PyObject_GenericGetAttr,/*tp_getattro*/
-        PyObject_GenericSetAttr,/*tp_setattro*/
-        0,                      /*tp_as_buffer*/
-        Py_TPFLAGS_DEFAULT,     /*tp_flags*/
-        0,                      /*tp_doc*/
-        0,                      /*tp_traverse*/
-        0,                      /*tp_clear*/
-        (richcmpfunc)Context_compare,    /*tp_richcompare*/
-        0,                      /*tp_weaklistoffset*/
-        0,                      /*tp_iter*/
-        0,                      /*tp_iternext*/
-        Context_methods,        /*tp_methods*/
-        0,                      /*tp_members*/
-        0,                      /*tp_getset*/
-        0,                      /*tp_base*/
-        0,                      /*tp_dict*/
-        0,                      /*tp_descr_get*/
-        0,                      /*tp_descr_set*/
-        0,                      /*tp_dictoffset*/
-        0,                      /*tp_init*/
-        PyType_GenericAlloc,    /*tp_alloc*/
-        PyType_GenericNew,      /*tp_new*/
-      	PyObject_Del,          /*tp_free*/
-        0,                      /*tp_is_gc*/
-};
-
-static PyObject *
-Context_FromContext(hexchat_context *context)
-{
-	ContextObject *ctxobj = PyObject_New(ContextObject, &Context_Type);
-	if (ctxobj != NULL)
-		ctxobj->context = context;
-	return (PyObject *) ctxobj;
-}
-
-static PyObject *
-Context_FromServerAndChannel(char *server, char *channel)
-{
-	ContextObject *ctxobj;
-	hexchat_context *context;
-	BEGIN_XCHAT_CALLS(NONE);
-	context = hexchat_find_context(ph, server, channel);
-	END_XCHAT_CALLS();
-	if (context == NULL)
-		return NULL;
-	ctxobj = PyObject_New(ContextObject, &Context_Type);
-	if (ctxobj == NULL)
-		return NULL;
-	ctxobj->context = context;
-	return (PyObject *) ctxobj;
-}
-
-
-/* ===================================================================== */
-/* ListItem object */
-
-#undef OFF
-#define OFF(x) offsetof(ListItemObject, x)
-
-static PyMemberDef ListItem_members[] = {
-	{"__dict__", T_OBJECT, OFF(dict), 0},
-	{0}
-};
-
-static void
-ListItem_dealloc(PyObject *self)
-{
-	Py_DECREF(((ListItemObject*)self)->dict);
-	Py_TYPE(self)->tp_free((PyObject *)self);
-}
-
-static PyObject *
-ListItem_repr(PyObject *self)
-{
-	return PyUnicode_FromFormat("<%s list item at %p>",
-			    	   ((ListItemObject*)self)->listname, self);
-}
-
-static PyTypeObject ListItem_Type = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	"hexchat.ListItem",	/*tp_name*/
-	sizeof(ListItemObject),	/*tp_basicsize*/
-	0,			/*tp_itemsize*/
-	ListItem_dealloc,	/*tp_dealloc*/
-	0,			/*tp_print*/
-	0,			/*tp_getattr*/
-	0,			/*tp_setattr*/
-	0,			/*tp_compare*/
-	ListItem_repr,		/*tp_repr*/
-	0,			/*tp_as_number*/
-	0,			/*tp_as_sequence*/
-	0,			/*tp_as_mapping*/
-	0,			/*tp_hash*/
-        0,                      /*tp_call*/
-        0,                      /*tp_str*/
-        PyObject_GenericGetAttr,/*tp_getattro*/
-        PyObject_GenericSetAttr,/*tp_setattro*/
-        0,                      /*tp_as_buffer*/
-        Py_TPFLAGS_DEFAULT,     /*tp_flags*/
-        0,                      /*tp_doc*/
-        0,                      /*tp_traverse*/
-        0,                      /*tp_clear*/
-        0,                      /*tp_richcompare*/
-        0,                      /*tp_weaklistoffset*/
-        0,                      /*tp_iter*/
-        0,                      /*tp_iternext*/
-        0,                      /*tp_methods*/
-        ListItem_members,       /*tp_members*/
-        0,                      /*tp_getset*/
-        0,                      /*tp_base*/
-        0,                      /*tp_dict*/
-        0,                      /*tp_descr_get*/
-        0,                      /*tp_descr_set*/
-        OFF(dict),              /*tp_dictoffset*/
-        0,                      /*tp_init*/
-        PyType_GenericAlloc,    /*tp_alloc*/
-        PyType_GenericNew,      /*tp_new*/
-      	PyObject_Del,          /*tp_free*/
-        0,                      /*tp_is_gc*/
-};
-
-static PyObject *
-ListItem_New(const char *listname)
-{
-	ListItemObject *item;
-	item = PyObject_New(ListItemObject, &ListItem_Type);
-	if (item != NULL) {
-		/* listname parameter must be statically allocated. */
-		item->listname = listname;
-		item->dict = PyDict_New();
-		if (item->dict == NULL) {
-			Py_DECREF(item);
-			item = NULL;
-		}
-	}
-	return (PyObject *) item;
-}
-
-
-/* ===================================================================== */
-/* Plugin object */
-
-#define GET_MODULE_DATA(x, force) \
-	o = PyObject_GetAttrString(m, "__module_" #x "__"); \
-	if (o == NULL) { \
-		if (force) { \
-			hexchat_print(ph, "Module has no __module_" #x "__ " \
-					"defined"); \
-			goto error; \
-		} \
-		plugin->x = g_strdup(""); \
-	} else {\
-		if (!PyUnicode_Check(o)) { \
-			hexchat_print(ph, "Variable __module_" #x "__ " \
-					"must be a string"); \
-			goto error; \
-		} \
-		plugin->x = g_strdup(PyUnicode_AsUTF8(o)); \
-		if (plugin->x == NULL) { \
-			hexchat_print(ph, "Not enough memory to allocate " #x); \
-			goto error; \
-		} \
-	}
-
-static PyObject *
-Plugin_GetCurrent()
-{
-	PyObject *plugin;
-	plugin = PySys_GetObject("__plugin__");
-	if (plugin == NULL)
-		PyErr_SetString(PyExc_RuntimeError, "lost sys.__plugin__");
-	return plugin;
-}
-
-static hexchat_plugin *
-Plugin_GetHandle(PluginObject *plugin)
-{
-	/* This works but the issue is that the script must be ran to get
-	 * the name of it thus upon first use it will use the wrong handler
-	 * work around would be to run a fake script once to get name? */
-#if 0
-	/* return fake handle for pluginpref */
-	if (plugin->gui != NULL)
-		return plugin->gui;
-	else
-#endif
-		return ph;
-}
-
-static PluginObject *
-Plugin_ByString(char *str)
-{
-	GSList *list;
-	PluginObject *plugin;
-	char *basename;
-	list = plugin_list;
-	while (list != NULL) {
-		plugin = (PluginObject *) list->data;
-		basename = g_path_get_basename(plugin->filename);
-		if (basename == NULL)
-			break;
-		if (strcasecmp(plugin->name, str) == 0 ||
-		    strcasecmp(plugin->filename, str) == 0 ||
-		    strcasecmp(basename, str) == 0) {
-			g_free(basename);
-			return plugin;
-		}
-		g_free(basename);
-		list = list->next;
-	}
-	return NULL;
-}
-
-static Hook *
-Plugin_AddHook(int type, PyObject *plugin, PyObject *callback,
-	       PyObject *userdata, char *name, void *data)
-{
-	Hook *hook = g_new(Hook, 1);
-	hook->type = type;
-	hook->plugin = plugin;
-	Py_INCREF(callback);
-	hook->callback = callback;
-	Py_INCREF(userdata);
-	hook->userdata = userdata;
-	hook->name = g_strdup (name);
-	hook->data = NULL;
-	Plugin_SetHooks(plugin, g_slist_append(Plugin_GetHooks(plugin),
-					       hook));
-
-	return hook;
-}
-
-static Hook *
-Plugin_FindHook(PyObject *plugin, char *name)
-{
-	Hook *hook = NULL;
-	GSList *plugin_hooks = Plugin_GetHooks(plugin);
-	
-	while (plugin_hooks)
-	{
-		if (g_strcmp0 (((Hook *)plugin_hooks->data)->name, name) == 0)
-		{
-			hook = (Hook *)plugin_hooks->data;
-			break;
-		}
-		
-		plugin_hooks = g_slist_next(plugin_hooks);
-	}
-
-	return hook;
-}
-
-static void
-Plugin_RemoveHook(PyObject *plugin, Hook *hook)
-{
-	GSList *list;
-	/* Is this really a hook of the running plugin? */
-	list = g_slist_find(Plugin_GetHooks(plugin), hook);
-	if (list) {
-		/* Ok, unhook it. */
-		if (hook->type != HOOK_UNLOAD) {
-			/* This is an xchat hook. Unregister it. */
-			BEGIN_XCHAT_CALLS(NONE);
-			hexchat_unhook(ph, (hexchat_hook*)hook->data);
-			END_XCHAT_CALLS();
-		}
-		Plugin_SetHooks(plugin,
-				g_slist_remove(Plugin_GetHooks(plugin),
-					       hook));
-		Py_DECREF(hook->callback);
-		Py_DECREF(hook->userdata);
-		g_free(hook->name);
-		g_free(hook);
-	}
-}
-
-static void
-Plugin_RemoveAllHooks(PyObject *plugin)
-{
-	GSList *list = Plugin_GetHooks(plugin);
-	while (list) {
-		Hook *hook = (Hook *) list->data;
-		if (hook->type != HOOK_UNLOAD) {
-			/* This is an xchat hook. Unregister it. */
-			BEGIN_XCHAT_CALLS(NONE);
-			hexchat_unhook(ph, (hexchat_hook*)hook->data);
-			END_XCHAT_CALLS();
-		}
-		Py_DECREF(hook->callback);
-		Py_DECREF(hook->userdata);
-		g_free(hook->name);
-		g_free(hook);
-		list = list->next;
-	}
-	Plugin_SetHooks(plugin, NULL);
-}
-
-static void
-Plugin_Delete(PyObject *plugin)
-{
-	PyThreadState *tstate = ((PluginObject*)plugin)->tstate;
-	GSList *list = Plugin_GetHooks(plugin);
-	while (list) {
-		Hook *hook = (Hook *) list->data;
-		if (hook->type == HOOK_UNLOAD) {
-			PyObject *retobj;
-			retobj = PyObject_CallFunction(hook->callback, "(O)",
-						       hook->userdata);
-			if (retobj) {
-				Py_DECREF(retobj);
-			} else {
-				PyErr_Print();
-				PyErr_Clear();
-			}
-		}
-		list = list->next;
-	}
-	Plugin_RemoveAllHooks(plugin);
-	if (((PluginObject *)plugin)->gui != NULL)
-		hexchat_plugingui_remove(ph, ((PluginObject *)plugin)->gui);
-	Py_DECREF(plugin);
-	/*PyThreadState_Swap(tstate); needed? */
-	Py_EndInterpreter(tstate);
-}
-
-static PyObject *
-Plugin_New(char *filename, PyObject *xcoobj)
-{
-	PluginObject *plugin = NULL;
-	PyObject *m, *o;
-#ifdef IS_PY3K
-	wchar_t *argv[] = { L"<hexchat>", 0 };
-#else
-	char *argv[] = { "<hexchat>", 0 };
-#endif
-
-	if (filename) {
-		char *old_filename = filename;
-		filename = Util_Expand(filename);
-		if (filename == NULL) {
-			hexchat_printf(ph, "File not found: %s", old_filename);
-			return NULL;
-		}
-	}
-
-	/* Allocate plugin structure. */
-	plugin = PyObject_New(PluginObject, &Plugin_Type);
-	if (plugin == NULL) {
-		hexchat_print(ph, "Can't create plugin object");
-		goto error;
-	}
-
-	Plugin_SetName(plugin, NULL);
-	Plugin_SetVersion(plugin, NULL);
-	Plugin_SetFilename(plugin, NULL);
-	Plugin_SetDescription(plugin, NULL);
-	Plugin_SetHooks(plugin, NULL);
-	Plugin_SetContext(plugin, hexchat_get_context(ph));
-	Plugin_SetGui(plugin, NULL);
-
-	/* Start a new interpreter environment for this plugin. */
-	PyEval_AcquireThread(main_tstate);
-	plugin->tstate = Py_NewInterpreter();
-	if (plugin->tstate == NULL) {
-		hexchat_print(ph, "Can't create interpreter state");
-		goto error;
-	}
-
-	PySys_SetArgv(1, argv);
-	PySys_SetObject("__plugin__", (PyObject *) plugin);
-
-	/* Set stdout and stderr to xchatout. */
-	Py_INCREF(xcoobj);
-	PySys_SetObject("stdout", xcoobj);
-	Py_INCREF(xcoobj);
-	PySys_SetObject("stderr", xcoobj);
-
-	if (filename) {
-#ifdef WIN32
-		char *file;
-		if (!g_file_get_contents(filename, &file, NULL, NULL)) {
-			hexchat_printf(ph, "Can't open file %s: %s\n",
-				     filename, strerror(errno));
-			goto error;
-		}
-
-		if (PyRun_SimpleString(file) != 0) {
-			hexchat_printf(ph, "Error loading module %s\n",
-				     filename);
-			g_free (file);
-			goto error;
-		}
-
-		plugin->filename = filename;
-		filename = NULL;
-		g_free (file);
-#else
-		FILE *fp;
-		plugin->filename = filename;
-
-		/* It's now owned by the plugin. */
-		filename = NULL;
-
-		/* Open the plugin file. */
-		fp = fopen(plugin->filename, "r");
-		if (fp == NULL) {
-			hexchat_printf(ph, "Can't open file %s: %s\n",
-				     plugin->filename, strerror(errno));
-			goto error;
-		}
-
-		/* Run the plugin. */
-		if (PyRun_SimpleFile(fp, plugin->filename) != 0) {
-			hexchat_printf(ph, "Error loading module %s\n",
-				     plugin->filename);
-			fclose(fp);
-			goto error;
-		}
-		fclose(fp);
-#endif
-		m = PyDict_GetItemString(PyImport_GetModuleDict(),
-					 "__main__");
-		if (m == NULL) {
-			hexchat_print(ph, "Can't get __main__ module");
-			goto error;
-		}
-		GET_MODULE_DATA(name, 1);
-		GET_MODULE_DATA(version, 0);
-		GET_MODULE_DATA(description, 0);
-		plugin->gui = hexchat_plugingui_add(ph, plugin->filename,
-						  plugin->name,
-						  plugin->description,
-						  plugin->version, NULL);
-	}
-
-	PyEval_ReleaseThread(plugin->tstate);
-
-	return (PyObject *) plugin;
-
-error:
-	g_free(filename);
-
-	if (plugin) {
-		if (plugin->tstate)
-			Plugin_Delete((PyObject *)plugin);
-		else
-			Py_DECREF(plugin);
-	}
-	PyEval_ReleaseLock();
-
-	return NULL;
-}
-
-static void
-Plugin_dealloc(PluginObject *self)
-{
-	g_free(self->filename);
-	g_free(self->name);
-	g_free(self->version);
-	g_free(self->description);
-	Py_TYPE(self)->tp_free((PyObject *)self);
-}
-
-static PyTypeObject Plugin_Type = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	"hexchat.Plugin",		/*tp_name*/
-	sizeof(PluginObject),	/*tp_basicsize*/
-	0,			/*tp_itemsize*/
-	(destructor)Plugin_dealloc, /*tp_dealloc*/
-	0,			/*tp_print*/
-	0,			/*tp_getattr*/
-	0,			/*tp_setattr*/
-	0,			/*tp_compare*/
-	0,			/*tp_repr*/
-	0,			/*tp_as_number*/
-	0,			/*tp_as_sequence*/
-	0,			/*tp_as_mapping*/
-	0,			/*tp_hash*/
-        0,                      /*tp_call*/
-        0,                      /*tp_str*/
-        PyObject_GenericGetAttr,/*tp_getattro*/
-        PyObject_GenericSetAttr,/*tp_setattro*/
-        0,                      /*tp_as_buffer*/
-        Py_TPFLAGS_DEFAULT,     /*tp_flags*/
-        0,                      /*tp_doc*/
-        0,                      /*tp_traverse*/
-        0,                      /*tp_clear*/
-        0,                      /*tp_richcompare*/
-        0,                      /*tp_weaklistoffset*/
-        0,                      /*tp_iter*/
-        0,                      /*tp_iternext*/
-        0,                      /*tp_methods*/
-        0,                      /*tp_members*/
-        0,                      /*tp_getset*/
-        0,                      /*tp_base*/
-        0,                      /*tp_dict*/
-        0,                      /*tp_descr_get*/
-        0,                      /*tp_descr_set*/
-        0,                      /*tp_dictoffset*/
-        0,                      /*tp_init*/
-        PyType_GenericAlloc,    /*tp_alloc*/
-        PyType_GenericNew,      /*tp_new*/
-      	PyObject_Del,          /*tp_free*/
-        0,                      /*tp_is_gc*/
-};
-
-
-/* ===================================================================== */
-/* XChat module */
-
-static PyObject *
-Module_hexchat_command(PyObject *self, PyObject *args)
-{
-	char *text;
-	if (!PyArg_ParseTuple(args, "s:command", &text))
-		return NULL;
-	BEGIN_XCHAT_CALLS(RESTORE_CONTEXT|ALLOW_THREADS);
-	hexchat_command(ph, text);
-	END_XCHAT_CALLS();
-	Py_RETURN_NONE;
-}
-
-static PyObject *
-Module_xchat_prnt(PyObject *self, PyObject *args)
-{
-	char *text;
-	if (!PyArg_ParseTuple(args, "s:prnt", &text))
-		return NULL;
-	BEGIN_XCHAT_CALLS(RESTORE_CONTEXT|ALLOW_THREADS);
-	hexchat_print(ph, text);
-	END_XCHAT_CALLS();
-	Py_RETURN_NONE;
-}
-
-static PyObject *
-Module_hexchat_emit_print(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *argv[6];
-	char *name;
-	int res;
-	long time = 0;
-	hexchat_event_attrs *attrs;
-	char *kwlist[] = {"name", "arg1", "arg2", "arg3",
-					"arg4", "arg5", "arg6", 
-					"time", NULL};
-	memset(&argv, 0, sizeof(char*)*6);
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|ssssssl:print_event", kwlist, &name,
-			      &argv[0], &argv[1], &argv[2],
-			      &argv[3], &argv[4], &argv[5],
-				  &time))
-		return NULL;
-	BEGIN_XCHAT_CALLS(RESTORE_CONTEXT|ALLOW_THREADS);
-	attrs = hexchat_event_attrs_create(ph);
-	attrs->server_time_utc = (time_t)time; 
-	
-	res = hexchat_emit_print_attrs(ph, attrs, name, argv[0], argv[1], argv[2],
-					 argv[3], argv[4], argv[5], NULL);
-
-	hexchat_event_attrs_free(ph, attrs);
-	END_XCHAT_CALLS();
-	return PyLong_FromLong(res);
-}
-
-static PyObject *
-Module_hexchat_get_info(PyObject *self, PyObject *args)
-{
-	const char *info;
-	char *name;
-	if (!PyArg_ParseTuple(args, "s:get_info", &name))
-		return NULL;
-	BEGIN_XCHAT_CALLS(RESTORE_CONTEXT);
-	info = hexchat_get_info(ph, name);
-	END_XCHAT_CALLS();
-	if (info == NULL) {
-		Py_RETURN_NONE;
-	}
-	if (strcmp (name, "gtkwin_ptr") == 0 || strcmp (name, "win_ptr") == 0)
-		return PyUnicode_FromFormat("%p", info); /* format as pointer */
-	else
-		return PyUnicode_FromString(info);
-}
-
-static PyObject *
-Module_xchat_get_prefs(PyObject *self, PyObject *args)
-{
-	PyObject *res;
-	const char *info;
-	int integer;
-	char *name;
-	int type;
-	if (!PyArg_ParseTuple(args, "s:get_prefs", &name))
-		return NULL;
-	BEGIN_XCHAT_CALLS(NONE);
-	type = hexchat_get_prefs(ph, name, &info, &integer);
-	END_XCHAT_CALLS();
-	switch (type) {
-		case 0:
-			Py_INCREF(Py_None);
-			res = Py_None;
-			break;
-		case 1:
-			res = PyUnicode_FromString((char*)info);
-			break;
-		case 2:
-		case 3:
-			res = PyLong_FromLong(integer);
-			break;
-		default:
-			PyErr_Format(PyExc_RuntimeError,
-				     "unknown get_prefs type (%d), "
-				     "please report", type);
-			res = NULL;
-			break;
-	}
-	return res;
-}
-
-static PyObject *
-Module_hexchat_get_context(PyObject *self, PyObject *args)
-{
-	PyObject *plugin;
-	PyObject *ctxobj;
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	ctxobj = Context_FromContext(Plugin_GetContext(plugin));
-	if (ctxobj == NULL) {
-		Py_RETURN_NONE;
-	}
-	return ctxobj;
-}
-
-static PyObject *
-Module_hexchat_find_context(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *server = NULL;
-	char *channel = NULL;
-	PyObject *ctxobj;
-	char *kwlist[] = {"server", "channel", 0};
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|zz:find_context",
-					 kwlist, &server, &channel))
-		return NULL;
-	ctxobj = Context_FromServerAndChannel(server, channel);
-	if (ctxobj == NULL) {
-		Py_RETURN_NONE;
-	}
-	return ctxobj;
-}
-
-static PyObject *
-Module_hexchat_pluginpref_set(PyObject *self, PyObject *args)
-{
-	PluginObject *plugin = (PluginObject*)Plugin_GetCurrent();
-	hexchat_plugin *prefph = Plugin_GetHandle(plugin);
-	int result;
-	char *var;
-	PyObject *value;
-		
-	if (!PyArg_ParseTuple(args, "sO:set_pluginpref", &var, &value))
-		return NULL;
-	if (PyLong_Check(value)) {
-		int intvalue = PyLong_AsLong(value);
-		BEGIN_XCHAT_CALLS(NONE);
-		result = hexchat_pluginpref_set_int(prefph, var, intvalue);
-		END_XCHAT_CALLS();
-	}
-	else if (PyUnicode_Check(value)) {
-		char *charvalue = PyUnicode_AsUTF8(value);
-		BEGIN_XCHAT_CALLS(NONE);
-		result = hexchat_pluginpref_set_str(prefph, var, charvalue);
-		END_XCHAT_CALLS();
-	}
-	else
-		result = 0;
-	return PyBool_FromLong(result);
-}
-
-static PyObject *
-Module_hexchat_pluginpref_get(PyObject *self, PyObject *args)
-{
-	PluginObject *plugin = (PluginObject*)Plugin_GetCurrent();
-	hexchat_plugin *prefph = Plugin_GetHandle(plugin);
-	PyObject *ret;
-	char *var;
-	char retstr[512];
-	int retint;
-	int result;
-	if (!PyArg_ParseTuple(args, "s:get_pluginpref", &var))
-		return NULL;
-		
-	/* This will always return numbers as integers. */
-	BEGIN_XCHAT_CALLS(NONE);
-	result = hexchat_pluginpref_get_str(prefph, var, retstr);
-	END_XCHAT_CALLS();
-	if (result) {
-		if (strlen (retstr) <= 12) {
-			BEGIN_XCHAT_CALLS(NONE);
-			retint = hexchat_pluginpref_get_int(prefph, var);
-			END_XCHAT_CALLS();
-			if ((retint == -1) && (strcmp(retstr, "-1") != 0))
-				ret = PyUnicode_FromString(retstr);
-			else
-				ret = PyLong_FromLong(retint);
-		} else
-			ret = PyUnicode_FromString(retstr);
-	}
-	else
-	{
-		Py_INCREF(Py_None);
-		ret = Py_None;
-	}
-	return ret;
-}
-
-static PyObject *
-Module_hexchat_pluginpref_delete(PyObject *self, PyObject *args)
-{
-	PluginObject *plugin = (PluginObject*)Plugin_GetCurrent();
-	hexchat_plugin *prefph = Plugin_GetHandle(plugin);
-	char *var;
-	int result;
-	if (!PyArg_ParseTuple(args, "s:del_pluginpref", &var))
-		return NULL;
-	BEGIN_XCHAT_CALLS(NONE);
-	result = hexchat_pluginpref_delete(prefph, var);
-	END_XCHAT_CALLS();
-	return PyBool_FromLong(result);
-}
-
-static PyObject *
-Module_hexchat_pluginpref_list(PyObject *self, PyObject *args)
-{
-	PluginObject *plugin = (PluginObject*)Plugin_GetCurrent();
-	hexchat_plugin *prefph = Plugin_GetHandle(plugin);
-	char list[4096];
-	char* token;
-	int result;
-	PyObject *pylist;
-	pylist = PyList_New(0);
-	BEGIN_XCHAT_CALLS(NONE);
-	result = hexchat_pluginpref_list(prefph, list);
-	END_XCHAT_CALLS();
-	if (result) {
-		token = strtok(list, ",");
-		while (token != NULL) {
-			PyList_Append(pylist, PyUnicode_FromString(token));
-			token = strtok (NULL, ",");
-		}
-	}
-	return pylist;
-}
-
-static PyObject *
-Module_hexchat_hook_command(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *name;
-	PyObject *callback;
-	PyObject *userdata = Py_None;
-	int priority = HEXCHAT_PRI_NORM;
-	char *help = NULL;
-	PyObject *plugin;
-	Hook *hook;
-	char *kwlist[] = {"name", "callback", "userdata",
-			  "priority", "help", 0};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|Oiz:hook_command",
-					 kwlist, &name, &callback, &userdata,
-					 &priority, &help))
-		return NULL;
-
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	if (!PyCallable_Check(callback)) {
-		PyErr_SetString(PyExc_TypeError, "callback is not callable");
-		return NULL;
-	}
-
-	hook = Plugin_AddHook(HOOK_XCHAT, plugin, callback, userdata, name, NULL);
-	if (hook == NULL)
-		return NULL;
-
-	BEGIN_XCHAT_CALLS(NONE);
-	hook->data = (void*)hexchat_hook_command(ph, name, priority,
-					       Callback_Command, help, hook);
-	END_XCHAT_CALLS();
-
-	return PyLong_FromVoidPtr(hook);
-}
-
-static PyObject *
-Module_hexchat_hook_server(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *name;
-	PyObject *callback;
-	PyObject *userdata = Py_None;
-	int priority = HEXCHAT_PRI_NORM;
-	PyObject *plugin;
-	Hook *hook;
-	char *kwlist[] = {"name", "callback", "userdata", "priority", 0};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|Oi:hook_server",
-					 kwlist, &name, &callback, &userdata,
-					 &priority))
-		return NULL;
-
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	if (!PyCallable_Check(callback)) {
-		PyErr_SetString(PyExc_TypeError, "callback is not callable");
-		return NULL;
-	}
-
-	hook = Plugin_AddHook(HOOK_XCHAT, plugin, callback, userdata, NULL, NULL);
-	if (hook == NULL)
-		return NULL;
-
-	BEGIN_XCHAT_CALLS(NONE);
-	hook->data = (void*)hexchat_hook_server_attrs(ph, name, priority,
-					      Callback_Server, hook);
-	END_XCHAT_CALLS();
-
-	return PyLong_FromVoidPtr(hook);
-}
-
-static PyObject *
-Module_hexchat_hook_server_attrs(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *name;
-	PyObject *callback;
-	PyObject *userdata = Py_None;
-	int priority = HEXCHAT_PRI_NORM;
-	PyObject *plugin;
-	Hook *hook;
-	char *kwlist[] = {"name", "callback", "userdata", "priority", 0};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|Oi:hook_server",
-					 kwlist, &name, &callback, &userdata,
-					 &priority))
-		return NULL;
-
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	if (!PyCallable_Check(callback)) {
-		PyErr_SetString(PyExc_TypeError, "callback is not callable");
-		return NULL;
-	}
-
-	hook = Plugin_AddHook(HOOK_XCHAT_ATTR, plugin, callback, userdata, NULL, NULL);
-	if (hook == NULL)
-		return NULL;
-
-	BEGIN_XCHAT_CALLS(NONE);
-	hook->data = (void*)hexchat_hook_server_attrs(ph, name, priority,
-					      Callback_Server, hook);
-	END_XCHAT_CALLS();
-
-	return PyLong_FromVoidPtr(hook);
-}
-
-static PyObject *
-Module_hexchat_hook_print(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *name;
-	PyObject *callback;
-	PyObject *userdata = Py_None;
-	int priority = HEXCHAT_PRI_NORM;
-	PyObject *plugin;
-	Hook *hook;
-	char *kwlist[] = {"name", "callback", "userdata", "priority", 0};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|Oi:hook_print",
-					 kwlist, &name, &callback, &userdata,
-					 &priority))
-		return NULL;
-
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	if (!PyCallable_Check(callback)) {
-		PyErr_SetString(PyExc_TypeError, "callback is not callable");
-		return NULL;
-	}
-
-	hook = Plugin_AddHook(HOOK_XCHAT, plugin, callback, userdata, name, NULL);
-	if (hook == NULL)
-		return NULL;
-
-	BEGIN_XCHAT_CALLS(NONE);
-	hook->data = (void*)hexchat_hook_print(ph, name, priority,
-					     Callback_Print, hook);
-	END_XCHAT_CALLS();
-
-	return PyLong_FromVoidPtr(hook);
-}
-
-static PyObject *
-Module_hexchat_hook_print_attrs(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	char *name;
-	PyObject *callback;
-	PyObject *userdata = Py_None;
-	int priority = HEXCHAT_PRI_NORM;
-	PyObject *plugin;
-	Hook *hook;
-	char *kwlist[] = {"name", "callback", "userdata", "priority", 0};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|Oi:hook_print_attrs",
-					 kwlist, &name, &callback, &userdata,
-					 &priority))
-		return NULL;
-
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	if (!PyCallable_Check(callback)) {
-		PyErr_SetString(PyExc_TypeError, "callback is not callable");
-		return NULL;
-	}
-
-	hook = Plugin_AddHook(HOOK_XCHAT_ATTR, plugin, callback, userdata, name, NULL);
-	if (hook == NULL)
-		return NULL;
-
-	BEGIN_XCHAT_CALLS(NONE);
-	hook->data = (void*)hexchat_hook_print_attrs(ph, name, priority,
-					     Callback_Print_Attrs, hook);
-	END_XCHAT_CALLS();
-
-	return PyLong_FromVoidPtr(hook);
-}
-
-static PyObject *
-Module_hexchat_hook_timer(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	int timeout;
-	PyObject *callback;
-	PyObject *userdata = Py_None;
-	PyObject *plugin;
-	Hook *hook;
-	char *kwlist[] = {"timeout", "callback", "userdata", 0};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "iO|O:hook_timer",
-					 kwlist, &timeout, &callback,
-					 &userdata))
-		return NULL;
-
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	if (!PyCallable_Check(callback)) {
-		PyErr_SetString(PyExc_TypeError, "callback is not callable");
-		return NULL;
-	}
-
-	hook = Plugin_AddHook(HOOK_XCHAT, plugin, callback, userdata, NULL, NULL);
-	if (hook == NULL)
-		return NULL;
-
-	BEGIN_XCHAT_CALLS(NONE);
-	hook->data = (void*)hexchat_hook_timer(ph, timeout,
-					     Callback_Timer, hook);
-	END_XCHAT_CALLS();
-
-	return PyLong_FromVoidPtr(hook);
-}
-
-static PyObject *
-Module_hexchat_hook_unload(PyObject *self, PyObject *args, PyObject *kwargs)
-{
-	PyObject *callback;
-	PyObject *userdata = Py_None;
-	PyObject *plugin;
-	Hook *hook;
-	char *kwlist[] = {"callback", "userdata", 0};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O:hook_unload",
-					 kwlist, &callback, &userdata))
-		return NULL;
-
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-	if (!PyCallable_Check(callback)) {
-		PyErr_SetString(PyExc_TypeError, "callback is not callable");
-		return NULL;
-	}
-
-	hook = Plugin_AddHook(HOOK_UNLOAD, plugin, callback, userdata, NULL, NULL);
-	if (hook == NULL)
-		return NULL;
-
-	return PyLong_FromVoidPtr(hook);
-}
-
-static PyObject *
-Module_hexchat_unhook(PyObject *self, PyObject *args)
-{
-	PyObject *plugin;
-	PyObject *obj;
-	Hook *hook;
-	if (!PyArg_ParseTuple(args, "O:unhook", &obj))
-		return NULL;
-	plugin = Plugin_GetCurrent();
-	if (plugin == NULL)
-		return NULL;
-
-	if (PyUnicode_Check (obj))
-	{
-		hook = Plugin_FindHook(plugin, PyUnicode_AsUTF8 (obj));
-		while (hook)
-		{
-			Plugin_RemoveHook(plugin, hook);
-			hook = Plugin_FindHook(plugin, PyUnicode_AsUTF8 (obj));
-		}
-	}
-	else
-	{
-		hook = (Hook *)PyLong_AsVoidPtr(obj);
-		Plugin_RemoveHook(plugin, hook);
-	}	
-
-	Py_RETURN_NONE;
-}
-
-static PyObject *
-Module_xchat_get_list(PyObject *self, PyObject *args)
-{
-	hexchat_list *list;
-	PyObject *l;
-	const char *name;
-	const char *const *fields;
-	int i;
-
-	if (!PyArg_ParseTuple(args, "s:get_list", &name))
-		return NULL;
-	/* This function is thread safe, and returns statically
-	 * allocated data. */
-	fields = hexchat_list_fields(ph, "lists");
-	for (i = 0; fields[i]; i++) {
-		if (strcmp(fields[i], name) == 0) {
-			/* Use the static allocated one. */
-			name = fields[i];
-			break;
-		}
-	}
-	if (fields[i] == NULL) {
-		PyErr_SetString(PyExc_KeyError, "list not available");
-		return NULL;
-	}
-	l = PyList_New(0);
-	if (l == NULL)
-		return NULL;
-	BEGIN_XCHAT_CALLS(RESTORE_CONTEXT);
-	list = hexchat_list_get(ph, (char*)name);
-	if (list == NULL)
-		goto error;
-	fields = hexchat_list_fields(ph, (char*)name);
-	while (hexchat_list_next(ph, list)) {
-		PyObject *o = ListItem_New(name);
-		if (o == NULL || PyList_Append(l, o) == -1) {
-			Py_XDECREF(o);
-			goto error;
-		}
-		Py_DECREF(o); /* l is holding a reference */
-		for (i = 0; fields[i]; i++) {
-			const char *fld = fields[i]+1;
-			PyObject *attr = NULL;
-			const char *sattr;
-			int iattr;
-			time_t tattr;
-			switch(fields[i][0]) {
-			case 's':
-				sattr = hexchat_list_str(ph, list, (char*)fld);
-				attr = PyUnicode_FromString(sattr?sattr:"");
-				break;
-			case 'i':
-				iattr = hexchat_list_int(ph, list, (char*)fld);
-				attr = PyLong_FromLong((long)iattr);
-				break;
-			case 't':
-				tattr = hexchat_list_time(ph, list, (char*)fld);
-				attr = PyLong_FromLong((long)tattr);
-				break;
-			case 'p':
-				sattr = hexchat_list_str(ph, list, (char*)fld);
-				if (strcmp(fld, "context") == 0) {
-					attr = Context_FromContext(
-						(hexchat_context*)sattr);
-					break;
-				}
-			default: /* ignore unknown (newly added?) types */
-				continue;
-			}
-			if (attr == NULL)
-				goto error;
-			PyObject_SetAttrString(o, (char*)fld, attr); /* add reference on attr in o */
-			Py_DECREF(attr); /* make o own attr */
-		}
-	}
-	hexchat_list_free(ph, list);
-	goto exit;
-error:
-	if (list)
-		hexchat_list_free(ph, list);
-	Py_DECREF(l);
-	l = NULL;
-
-exit:
-	END_XCHAT_CALLS();
-	return l;
-}
-
-static PyObject *
-Module_xchat_get_lists(PyObject *self, PyObject *args)
-{
-	PyObject *l, *o;
-	const char *const *fields;
-	int i;
-	/* This function is thread safe, and returns statically
-	 * allocated data. */
-	fields = hexchat_list_fields(ph, "lists");
-	l = PyList_New(0);
-	if (l == NULL)
-		return NULL;
-	for (i = 0; fields[i]; i++) {
-		o = PyUnicode_FromString(fields[i]);
-		if (o == NULL || PyList_Append(l, o) == -1) {
-			Py_DECREF(l);
-			Py_XDECREF(o);
-			return NULL;
-		}
-		Py_DECREF(o); /* l is holding a reference */
-	}
-	return l;
-}
-
-static PyObject *
-Module_hexchat_nickcmp(PyObject *self, PyObject *args)
-{
-	char *s1, *s2;
-	if (!PyArg_ParseTuple(args, "ss:nickcmp", &s1, &s2))
-		return NULL;
-	return PyLong_FromLong((long) hexchat_nickcmp(ph, s1, s2));
-}
-
-static PyObject *
-Module_hexchat_strip(PyObject *self, PyObject *args)
-{
-	PyObject *result;
-	char *str, *str2;
-	int len = -1, flags = 1 | 2;
-	if (!PyArg_ParseTuple(args, "s|ii:strip", &str, &len, &flags))
-		return NULL;
-	str2 = hexchat_strip(ph, str, len, flags);
-	result = PyUnicode_FromString(str2);
-	hexchat_free(ph, str2);
-	return result;
-}
-
-static PyMethodDef Module_xchat_methods[] = {
-	{"command",		Module_hexchat_command,
-		METH_VARARGS},
-	{"prnt",		Module_xchat_prnt,
-		METH_VARARGS},
-	{"emit_print",		(PyCFunction)Module_hexchat_emit_print,
-		METH_VARARGS|METH_KEYWORDS},
-	{"get_info",		Module_hexchat_get_info,
-		METH_VARARGS},
-	{"get_prefs",		Module_xchat_get_prefs,
-		METH_VARARGS},
-	{"get_context",		Module_hexchat_get_context,
-		METH_NOARGS},
-	{"find_context",	(PyCFunction)Module_hexchat_find_context,
-		METH_VARARGS|METH_KEYWORDS},
-	{"set_pluginpref", Module_hexchat_pluginpref_set,
-		METH_VARARGS},
-	{"get_pluginpref", Module_hexchat_pluginpref_get,
-		METH_VARARGS},
-	{"del_pluginpref", Module_hexchat_pluginpref_delete,
-		METH_VARARGS},
-	{"list_pluginpref", Module_hexchat_pluginpref_list,
-		METH_VARARGS},
-	{"hook_command",	(PyCFunction)Module_hexchat_hook_command,
-		METH_VARARGS|METH_KEYWORDS},
-	{"hook_server",		(PyCFunction)Module_hexchat_hook_server,
-		METH_VARARGS|METH_KEYWORDS},
-	{"hook_server_attrs",		(PyCFunction)Module_hexchat_hook_server_attrs,
-		METH_VARARGS|METH_KEYWORDS},
-	{"hook_print",		(PyCFunction)Module_hexchat_hook_print,
-		METH_VARARGS|METH_KEYWORDS},
-	{"hook_print_attrs",		(PyCFunction)Module_hexchat_hook_print_attrs,
-		METH_VARARGS|METH_KEYWORDS},
-	{"hook_timer",		(PyCFunction)Module_hexchat_hook_timer,
-		METH_VARARGS|METH_KEYWORDS},
-	{"hook_unload",		(PyCFunction)Module_hexchat_hook_unload,
-		METH_VARARGS|METH_KEYWORDS},
-	{"unhook",		Module_hexchat_unhook,
-		METH_VARARGS},
-	{"get_list",		Module_xchat_get_list,
-		METH_VARARGS},
-	{"get_lists",		Module_xchat_get_lists,
-		METH_NOARGS},
-	{"nickcmp",		Module_hexchat_nickcmp,
-		METH_VARARGS},
-	{"strip",		Module_hexchat_strip,
-		METH_VARARGS},
-	{NULL, NULL}
-};
-
-#ifdef IS_PY3K
-static struct PyModuleDef moduledef = {
-	PyModuleDef_HEAD_INIT,
-	"hexchat",     /* m_name */
-	"HexChat Scripting Interface",  /* m_doc */
-	-1,                  /* m_size */
-	Module_xchat_methods,    /* m_methods */
-	NULL,                /* m_reload */
-	NULL,                /* m_traverse */
-	NULL,                /* m_clear */
-	NULL,                /* m_free */
-};
-
-static struct PyModuleDef xchat_moduledef = {
-	PyModuleDef_HEAD_INIT,
-	"xchat",     /* m_name */
-	"HexChat Scripting Interface",  /* m_doc */
-	-1,                  /* m_size */
-	Module_xchat_methods,    /* m_methods */
-	NULL,                /* m_reload */
-	NULL,                /* m_traverse */
-	NULL,                /* m_clear */
-	NULL,                /* m_free */
-};
-#endif
-
-static PyObject *
-moduleinit_hexchat(void)
-{
-	PyObject *hm;
-#ifdef IS_PY3K
-		hm = PyModule_Create(&moduledef);
-#else
-    hm = Py_InitModule3("hexchat", Module_xchat_methods, "HexChat Scripting Interface");
-#endif
-
-	PyModule_AddIntConstant(hm, "EAT_NONE", HEXCHAT_EAT_NONE);
-	PyModule_AddIntConstant(hm, "EAT_HEXCHAT", HEXCHAT_EAT_HEXCHAT);
-	PyModule_AddIntConstant(hm, "EAT_XCHAT", HEXCHAT_EAT_HEXCHAT); /* for compat */
-	PyModule_AddIntConstant(hm, "EAT_PLUGIN", HEXCHAT_EAT_PLUGIN);
-	PyModule_AddIntConstant(hm, "EAT_ALL", HEXCHAT_EAT_ALL);
-	PyModule_AddIntConstant(hm, "PRI_HIGHEST", HEXCHAT_PRI_HIGHEST);
-	PyModule_AddIntConstant(hm, "PRI_HIGH", HEXCHAT_PRI_HIGH);
-	PyModule_AddIntConstant(hm, "PRI_NORM", HEXCHAT_PRI_NORM);
-	PyModule_AddIntConstant(hm, "PRI_LOW", HEXCHAT_PRI_LOW);
-	PyModule_AddIntConstant(hm, "PRI_LOWEST", HEXCHAT_PRI_LOWEST);
-
-	PyObject_SetAttrString(hm, "__version__", Py_BuildValue("(ii)", VERSION_MAJOR, VERSION_MINOR));
-
-	return hm;
-}
-
-static PyObject *
-moduleinit_xchat(void)
-{
-	PyObject *xm;
-#ifdef IS_PY3K
-		xm = PyModule_Create(&xchat_moduledef);
-#else
-    xm = Py_InitModule3("xchat", Module_xchat_methods, "HexChat Scripting Interface");
-#endif
-
-	PyModule_AddIntConstant(xm, "EAT_NONE", HEXCHAT_EAT_NONE);
-	PyModule_AddIntConstant(xm, "EAT_XCHAT", HEXCHAT_EAT_HEXCHAT);
-	PyModule_AddIntConstant(xm, "EAT_PLUGIN", HEXCHAT_EAT_PLUGIN);
-	PyModule_AddIntConstant(xm, "EAT_ALL", HEXCHAT_EAT_ALL);
-	PyModule_AddIntConstant(xm, "PRI_HIGHEST", HEXCHAT_PRI_HIGHEST);
-	PyModule_AddIntConstant(xm, "PRI_HIGH", HEXCHAT_PRI_HIGH);
-	PyModule_AddIntConstant(xm, "PRI_NORM", HEXCHAT_PRI_NORM);
-	PyModule_AddIntConstant(xm, "PRI_LOW", HEXCHAT_PRI_LOW);
-	PyModule_AddIntConstant(xm, "PRI_LOWEST", HEXCHAT_PRI_LOWEST);
-
-	PyObject_SetAttrString(xm, "__version__", Py_BuildValue("(ii)", VERSION_MAJOR, VERSION_MINOR));
-
-	return xm;
-}
-
-#ifdef IS_PY3K
-PyMODINIT_FUNC
-PyInit_hexchat(void)
-{
-    return moduleinit_hexchat();
-}
-PyMODINIT_FUNC
-PyInit_xchat(void)
-{
-    return moduleinit_xchat();
-}
-#else
-PyMODINIT_FUNC
-inithexchat(void)
-{
-		moduleinit_hexchat();
-}
-PyMODINIT_FUNC
-initxchat(void)
-{
-		moduleinit_xchat();
-}
-#endif
-
-/* ===================================================================== */
-/* Python interactive interpreter functions */
-
-static void
-IInterp_Exec(char *command)
-{
-	PyObject *m, *d, *o;
-	char *buffer;
-	int len;
-
-	BEGIN_PLUGIN(interp_plugin);
-
-	m = PyImport_AddModule("__main__");
-	if (m == NULL) {
-		hexchat_print(ph, "Can't get __main__ module");
-		goto fail;
-	}
-	d = PyModule_GetDict(m);
-	len = strlen(command);
-
-	buffer = g_malloc(len + 2);
-	memcpy(buffer, command, len);
-	buffer[len] = '\n';
-	buffer[len+1] = 0;
-	PyRun_SimpleString("import hexchat");
-	o = PyRun_StringFlags(buffer, Py_single_input, d, d, NULL);
-	g_free(buffer);
-	if (o == NULL) {
-		PyErr_Print();
-		goto fail;
-	}
-	Py_DECREF(o);
-
-fail:
-	END_PLUGIN(interp_plugin);
-	return;
-}
-
-static int
-IInterp_Cmd(char *word[], char *word_eol[], void *userdata)
-{
-	char *channel = (char *) hexchat_get_info(ph, "channel");
-	if (channel && channel[0] == '>' && strcmp(channel, ">>python<<") == 0) {
-		hexchat_printf(ph, ">>> %s\n", word_eol[1]);
-		IInterp_Exec(word_eol[1]);
-		return HEXCHAT_EAT_HEXCHAT;
-	}
-	return HEXCHAT_EAT_NONE;
-}
-
-
-/* ===================================================================== */
-/* Python command handling */
-
-static void
-Command_PyList(void)
-{
-	GSList *list;
-	list = plugin_list;
-	if (list == NULL) {
-		hexchat_print(ph, "No python modules loaded");
-	} else {
-		hexchat_print(ph,
-		   "Name         Version  Filename             Description\n"
-		   "----         -------  --------             -----------\n");
-		while (list != NULL) {
-			PluginObject *plg = (PluginObject *) list->data;
-			char *basename = g_path_get_basename(plg->filename);
-			hexchat_printf(ph, "%-12s %-8s %-20s %-10s\n",
-				     plg->name,
-				     *plg->version ? plg->version
-				     		  : "<none>",
-				     basename,
-				     *plg->description ? plg->description
-				     		      : "<none>");
-			g_free(basename);
-			list = list->next;
-		}
-		hexchat_print(ph, "\n");
-	}
-}
-
-static void
-Command_PyLoad(char *filename)
-{
-	PyObject *plugin;
-	RELEASE_XCHAT_LOCK();
-	plugin = Plugin_New(filename, xchatout);
-	ACQUIRE_XCHAT_LOCK();
-	if (plugin)
-		plugin_list = g_slist_append(plugin_list, plugin);
-}
-
-static void
-Command_PyUnload(char *name)
-{
-	PluginObject *plugin = Plugin_ByString(name);
-	if (!plugin) {
-		hexchat_print(ph, "Can't find a python plugin with that name");
-	} else {
-		BEGIN_PLUGIN(plugin);
-		Plugin_Delete((PyObject*)plugin);
-		END_PLUGIN(plugin);
-		plugin_list = g_slist_remove(plugin_list, plugin);
-	}
-}
-
-static void
-Command_PyReload(char *name)
-{
-	PluginObject *plugin = Plugin_ByString(name);
-	if (!plugin) {
-		hexchat_print(ph, "Can't find a python plugin with that name");
-	} else {
-		char *filename = g_strdup(plugin->filename);
-		Command_PyUnload(filename);
-		Command_PyLoad(filename);
-		g_free(filename);
-	}
-}
-
-static void
-Command_PyAbout(void)
-{
-	hexchat_print(ph, about);
-}
-
-static int
-Command_Py(char *word[], char *word_eol[], void *userdata)
-{
-	char *cmd = word[2];
-	int ok = 0;
-	if (strcasecmp(cmd, "LIST") == 0) {
-		ok = 1;
-		Command_PyList();
-	} else if (strcasecmp(cmd, "EXEC") == 0) {
-		if (word[3][0]) {
-			ok = 1;
-			IInterp_Exec(word_eol[3]);
-		}
-	} else if (strcasecmp(cmd, "LOAD") == 0) {
-		if (word[3][0]) {
-			ok = 1;
-			Command_PyLoad(word[3]);
-		}
-	} else if (strcasecmp(cmd, "UNLOAD") == 0) {
-		if (word[3][0]) {
-			ok = 1;
-			Command_PyUnload(word[3]);
-		}
-	} else if (strcasecmp(cmd, "RELOAD") == 0) {
-		if (word[3][0]) {
-			ok = 1;
-			Command_PyReload(word[3]);
-		}
-	} else if (strcasecmp(cmd, "CONSOLE") == 0) {
-		ok = 1;
-		hexchat_command(ph, "QUERY >>python<<");
-	} else if (strcasecmp(cmd, "ABOUT") == 0) {
-		ok = 1;
-		Command_PyAbout();
-	}
-	if (!ok)
-		hexchat_print(ph, usage);
-	return HEXCHAT_EAT_ALL;
-}
-
-static int
-Command_Load(char *word[], char *word_eol[], void *userdata)
-{
-	int len = strlen(word[2]);
-	if (len > 3 && strcasecmp(".py", word[2]+len-3) == 0) {
-		Command_PyLoad(word[2]);
-		return HEXCHAT_EAT_HEXCHAT;
-	}
-	return HEXCHAT_EAT_NONE;
-}
-
-static int
-Command_Reload(char *word[], char *word_eol[], void *userdata)
-{
-	int len = strlen(word[2]);
-	if (len > 3 && strcasecmp(".py", word[2]+len-3) == 0) {
-	Command_PyReload(word[2]);
-	return HEXCHAT_EAT_HEXCHAT;
-	}
-	return HEXCHAT_EAT_NONE;
-}
-
-static int
-Command_Unload(char *word[], char *word_eol[], void *userdata)
-{
-	int len = strlen(word[2]);
-	if (len > 3 && strcasecmp(".py", word[2]+len-3) == 0) {
-		Command_PyUnload(word[2]);
-		return HEXCHAT_EAT_HEXCHAT;
-	}
-	return HEXCHAT_EAT_NONE;
-}
-
-/* ===================================================================== */
-/* Autoload function */
-
-/* ===================================================================== */
-/* (De)initialization functions */
-
-static int initialized = 0;
-static int reinit_tried = 0;
-
-void
-hexchat_plugin_get_info(char **name, char **desc, char **version, void **reserved)
-{
-	*name = "Python";
-	*version = VERSION;
-	*desc = "Python scripting interface";
-   if (reserved)
-      *reserved = NULL;
-}
-
-int
-hexchat_plugin_init(hexchat_plugin *plugin_handle,
-		  char **plugin_name,
-		  char **plugin_desc,
-		  char **plugin_version,
-		  char *arg)
-{
-#ifdef IS_PY3K
-	wchar_t *argv[] = { L"<hexchat>", 0 };
-#else
-	char *argv[] = { "<hexchat>", 0 };
-#endif
-
-	ph = plugin_handle;
-
-	/* Block double initalization. */
-	if (initialized != 0) {
-		hexchat_print(ph, "Python interface already loaded");
-		/* deinit is called even when init fails, so keep track
-		 * of a reinit failure. */
-		reinit_tried++;
-		return 0;
-	}
-	initialized = 1;
-
-	*plugin_name = "Python";
-	*plugin_version = VERSION;
-
-	/* FIXME You can't free this since it's used as long as the plugin's
-	 * loaded, but if you unload it, everything belonging to the plugin is
-	 * supposed to be freed anyway.
-	 */
-	*plugin_desc = g_strdup_printf ("Python %d scripting interface", PY_MAJOR_VERSION);
-
-	/* Initialize python. */
-#ifdef IS_PY3K
-	Py_SetProgramName(L"hexchat");
-	PyImport_AppendInittab("hexchat", PyInit_hexchat);
-	PyImport_AppendInittab("xchat", PyInit_xchat);
-#else
-	Py_SetProgramName("hexchat");
-	PyImport_AppendInittab("hexchat", inithexchat);
-	PyImport_AppendInittab("xchat", initxchat);
-#endif
-	Py_Initialize();
-	PySys_SetArgv(1, argv);
-
-	xchatout_buffer = g_string_new (NULL);
-	xchatout = XChatOut_New();
-	if (xchatout == NULL) {
-		hexchat_print(ph, "Can't allocate xchatout object");
-		return 0;
-	}
-
-#ifdef WITH_THREAD
-	PyEval_InitThreads();
-	xchat_lock = PyThread_allocate_lock();
-	if (xchat_lock == NULL) {
-		hexchat_print(ph, "Can't allocate hexchat lock");
-		Py_DECREF(xchatout);
-		xchatout = NULL;
-		return 0;
-	}
-#endif
-
-	main_tstate = PyEval_SaveThread();
-
-	interp_plugin = Plugin_New(NULL, xchatout);
-	if (interp_plugin == NULL) {
-		hexchat_print(ph, "Plugin_New() failed.\n");
-#ifdef WITH_THREAD
-		PyThread_free_lock(xchat_lock);
-#endif
-		Py_DECREF(xchatout);
-		xchatout = NULL;
-		return 0;
-	}
-
-
-	hexchat_hook_command(ph, "", HEXCHAT_PRI_NORM, IInterp_Cmd, 0, 0);
-	hexchat_hook_command(ph, "PY", HEXCHAT_PRI_NORM, Command_Py, usage, 0);
-	hexchat_hook_command(ph, "LOAD", HEXCHAT_PRI_NORM, Command_Load, 0, 0);
-	hexchat_hook_command(ph, "UNLOAD", HEXCHAT_PRI_NORM, Command_Unload, 0, 0);
-	hexchat_hook_command(ph, "RELOAD", HEXCHAT_PRI_NORM, Command_Reload, 0, 0);
-#ifdef WITH_THREAD
-	thread_timer = hexchat_hook_timer(ph, 300, Callback_ThreadTimer, NULL);
-#endif
-
-	hexchat_print(ph, "Python interface loaded\n");
-
-	Util_Autoload();
-	return 1;
-}
-
-int
-hexchat_plugin_deinit(void)
-{
-	GSList *list;
-
-	/* A reinitialization was tried. Just give up and live the
-	 * environment as is. We are still alive. */
-	if (reinit_tried) {
-		reinit_tried--;
-		return 1;
-	}
-
-	list = plugin_list;
-	while (list != NULL) {
-		PyObject *plugin = (PyObject *) list->data;
-		BEGIN_PLUGIN(plugin);
-		Plugin_Delete(plugin);
-		END_PLUGIN(plugin);
-		list = list->next;
-	}
-	g_slist_free(plugin_list);
-	plugin_list = NULL;
-
-	/* Reset xchatout buffer. */
-	g_string_free (xchatout_buffer, TRUE);
-	xchatout_buffer = NULL;
-
-	if (interp_plugin) {
-		Py_DECREF(interp_plugin);
-		interp_plugin = NULL;
-	}
-
-	/* Switch back to the main thread state. */
-	if (main_tstate) {
-		PyEval_RestoreThread(main_tstate);
-		PyThreadState_Swap(main_tstate);
-		main_tstate = NULL;
-	}
-	Py_Finalize();
-
-#ifdef WITH_THREAD
-	if (thread_timer != NULL) {
-		hexchat_unhook(ph, thread_timer);
-		thread_timer = NULL;
-	}
-	PyThread_free_lock(xchat_lock);
-#endif
-
-	hexchat_print(ph, "Python interface unloaded\n");
-	initialized = 0;
-
-	return 1;
-}
-
diff --git a/plugins/python/python.def b/plugins/python/python.def
index 6ce04e98..e560f50f 100644
--- a/plugins/python/python.def
+++ b/plugins/python/python.def
@@ -1,4 +1,3 @@
 EXPORTS 

 hexchat_plugin_init 

 hexchat_plugin_deinit 

-hexchat_plugin_get_info 

diff --git a/plugins/python/python.py b/plugins/python/python.py
new file mode 100644
index 00000000..30694802
--- /dev/null
+++ b/plugins/python/python.py
@@ -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) as f:
+                data = f.read()
+            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, 1, -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) is 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
diff --git a/plugins/python/python2.vcxproj b/plugins/python/python2.vcxproj
index f914a865..42895ce4 100644
--- a/plugins/python/python2.vcxproj
+++ b/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">

@@ -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>

diff --git a/plugins/python/python3.vcxproj b/plugins/python/python3.vcxproj
index 815dc8b1..3eb86c2a 100644
--- a/plugins/python/python3.vcxproj
+++ b/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">

@@ -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>
\ No newline at end of file
diff --git a/plugins/python/python3.vcxproj.filters b/plugins/python/python3.vcxproj.filters
index 9165e798..5c5834bb 100644
--- a/plugins/python/python3.vcxproj.filters
+++ b/plugins/python/python3.vcxproj.filters
@@ -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>
\ No newline at end of file
diff --git a/plugins/python/python_style_guide.md b/plugins/python/python_style_guide.md
new file mode 100644
index 00000000..41db2474
--- /dev/null
+++ b/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',
+}
+```
diff --git a/plugins/python/xchat.py b/plugins/python/xchat.py
new file mode 100644
index 00000000..6922490b
--- /dev/null
+++ b/plugins/python/xchat.py
@@ -0,0 +1 @@
+from _hexchat import *
diff --git a/plugins/sysinfo/meson.build b/plugins/sysinfo/meson.build
index 8b67da2e..7e2cdb6c 100644
--- a/plugins/sysinfo/meson.build
+++ b/plugins/sysinfo/meson.build
@@ -13,13 +13,13 @@ sysinfo_includes = []
 sysinfo_cargs = []
 
 system = host_machine.system()
-if system == 'linux' or system == 'gnu' or system.startswith('gnu/') 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' or system == 'gnu' or system.startswith('gnu/')
+  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
diff --git a/plugins/sysinfo/shared/df.c b/plugins/sysinfo/shared/df.c
index ce0760a6..bb2c2131 100644
--- a/plugins/sysinfo/shared/df.c
+++ b/plugins/sysinfo/shared/df.c
@@ -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;
 
diff --git a/plugins/sysinfo/sysinfo.vcxproj b/plugins/sysinfo/sysinfo.vcxproj
index b1c7c8f0..a3ff0f8a 100644
--- a/plugins/sysinfo/sysinfo.vcxproj
+++ b/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>

diff --git a/plugins/upd/upd.vcxproj b/plugins/upd/upd.vcxproj
index 16f96e45..5dc497b4 100644
--- a/plugins/upd/upd.vcxproj
+++ b/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">

diff --git a/plugins/winamp/winamp.vcxproj b/plugins/winamp/winamp.vcxproj
index ccc04e72..78367ae9 100644
--- a/plugins/winamp/winamp.vcxproj
+++ b/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">