Compare commits

...

206 Commits

Author SHA1 Message Date
07873ca4e9 Migrate from gtk_widget_is_composited to gdk_screen_is_composited.
gtk_widget_is_composited is gone in GTK 3.
2023-01-28 11:30:07 -06:00
bde8d9d20a Remove gtk_tree_view_set_rules_hint.
This function is deprecated and is ignored by GTK 3. It also does
not really do anything with most modern GTK2 themes either.
2023-01-28 11:30:07 -06:00
c550dc9cb1 Migrate from gdk_drawable_get_size to gdk_window_get_{width,height}.
gdk_drawable_get_size is gone in GDK 3.
2023-01-28 11:30:07 -06:00
df8f516a40 Migrate from gdk_beep to gdk_desktop_beep.
gdk_beep is gone in GDK 3.
2023-01-28 11:30:07 -06:00
67b25fddf1 Add option to exclude nickname in window title (#2759) 2023-01-24 17:03:53 -06:00
bb7a03e9f6 Fix updating the topic (user@host) of dialogs on CHGHOST.
This is updated when a user receives a new message but would have
not been updated when a user sent a CHGHOST.
2022-11-05 12:42:51 -05:00
9d175cc459 Also request the extended-monitor capability.
This allows getting hostname/awaymsg/etc updates for monitored clients
which will update the internal cache for that data.
2022-11-05 12:42:51 -05:00
4ad7afe884 ci: Add gtk-update-icon-cache to msys2 workflow 2022-10-29 15:57:29 -05:00
221283ba19 add shortcut options in setup
will allow the user to control on which of the shortcuts to create:
* start menu - will now show the relevant window
* desktop
* quick launch

on uninstall, all will be removed on uninstall
2022-10-29 15:42:30 -05:00
8cf2aa5586 Make it more clear that /SERVER and /SERVCHAN use SSL by default. 2022-10-16 15:35:43 -05:00
8fb0d2311f Default /SERVER and friends to use SSL when built with OpenSSL.
Since commit 747a52aae8 users have to
opt-out of using SSL when creating a new server. This commit makes
it so /SERVER also uses SSL by default.

In order to connect insecurely users must now use one of these
methods:

    /SERVER -insecure irc.example.com
    /SERVER irc.example.com -6667

The `-ssl` flag and the `+port` syntax have been retained for compat
reasons.
2022-10-16 15:35:43 -05:00
2dbc6adbc2 Fix PROTOCTL NAMESX and only send when not using multi-prefix.
This capability is the equivalent of the old protoctl token.
2022-09-22 12:07:07 -05:00
bd4290a1a9 Support whitespace between language codes
So far, when configuring multiple spell check languages, Hexchat
requires the user to separate multiple entries with commas and
only commas. This patch allows users to also enter whitespace, e.g.

  de_DE, en_US

as is common in many applications.
2022-09-20 18:08:37 -05:00
46c9df1863 Fix various compiler warnings.
fish.c: -Wincompatible-pointer-types
fkeys.c: -Wmisleading-indentation
proto-irc.c: -Wincompatible-pointer-types
util.c: -Wdeprecated-declarations
xtext.c: -Wmaybe-uninitialized
2022-08-29 13:50:03 -05:00
20c50fd7ef notification_plugin_deinit: Set function signature to int(void *)
Previously the function signature was inconsistent throughout
src/fe-gtk/plugin-notification.{h,c}: One file had the signature
int(void), while the other had int(void *). Since this type mismatch
might lead to problems (especially with LTO) and the (possibly provided)
function argument isn't used in the function's definition, this commit
sets int(void *) as function signature for both the declaration and
definition of the function.

Fixes: https://github.com/hexchat/hexchat/issues/2726
2022-08-26 12:40:15 -05:00
d7c6c424e8 servlist: Remove ACN
They are no longer supporting TLS and let their cert expire.

Non-TLS networks do not belong in our default list.

Closes #2722
2022-08-01 14:07:31 -05:00
ec9653e754 servlist: Remove IRCHighWay
They have self-signed certs which doesn't belong in our default list.
2022-07-15 13:17:25 -05:00
dfda8f2eee fix sysinfo print of cpu name
the cpu name might have tailing spaces in Windows, which weren't remove before printing.
2022-05-14 12:42:46 -05:00
b8645bfbf2 Split long SASL auth strings into 400-byte chunks (#2709)
Fixes #2705
2022-05-13 15:56:26 -05:00
778047bc65 raise the max length of a server password to 1024
- alleviate #1296
2022-05-10 12:31:12 -05:00
2638c88479 update python3 from 3.6 to 3.8.10
Signed-off-by: DjLegolas <djlegolas@protonmail.com>
2022-05-08 14:20:58 -05:00
6da8f97e37 fix addons load in python2 2022-05-07 11:34:47 -05:00
2dd7636134 appdata: Update appstream usage for desktop applications 2022-04-30 16:47:04 -05:00
13b6a40b9c Change preferences sub-dialogs to be modal
This solves the issue where the parent dialog is closed and then
the child dialog is used.

This is however only a partial fix:

- Many other dialogs throughout the codebase do not currently have
  parent windows and need to be refactored.

- Not all window managers respect modal so users can still trigger
  bugs. We can be more defensive against this but it requires more
  refactoring.

Closes #2686
2022-04-16 18:41:34 -05:00
dd167b4c83 python: Fix API break in hook_timer()
Closes #2691
2022-04-15 13:43:22 -05:00
133f628064 Display common help numerics as SERVTEXT.
This makes it a lot easier for users to actually read.
2022-04-02 12:17:54 -05:00
d99a98ff4c notification: Don't print failure to load backend in UI
This isn't actually helpful information to users generally

Closes #2152
Closes #2684
2022-03-26 11:18:00 -05:00
94efa378f7 Reverse the notify.conf linked list before writing
hexchat populates the single linked list `notify_list` defined in
`src/common/notify.c` from `notify.conf` file. Each new line read from
the file is added to the list by `g_slist_prepend()` which adds it to
the front of the list. But in `notify_save()` the list elements are read
from the start to end of the list and written to the `notify.conf`. This
means everytime hexchat is opened and closed, the contents of
`notify.conf` get reversed. This commit creates a
temporary glist in `notify_save()` and applies `g_slist_reverse()` on it
and writes the contents of this reversed list to `notify.conf`. And
solves issue #2680
2022-02-17 10:24:59 -06:00
ccf6f431bb Return userdata from pluginprefs __pairs metamethod to avoid immediate GC. 2022-02-16 12:44:09 -06:00
73c0b672a2 Bump to 2.16.1 2022-02-12 12:28:17 -06:00
7cff05c7ac Add -q/-- flags to /execwrite to EXECWRITE and cmd_execW (#2675)
added two flags to EXECWRITE and cmd_execw
-q : (quiet) to allow suppressing of additional (debug) output at the text box
--  : (stop parsing for further flags) for the edge cases where -q itself migh be part of used data and the user wants to show that at the text box

Closes #2666
2022-01-24 11:38:21 -06:00
1de339dfbc meson: Fix exported functions in plugins
This adds DEF file names in meson. Without the DEF files, every
functions are exproted from plugins.
2022-01-21 15:54:49 -06:00
a330c1cf4d sysinfo: Fix architecture detection in AArch64 Windows
AArch64 should be detected as 64 bit OS.
2022-01-21 15:54:30 -06:00
7df34cdcb2 Log when the user specifies an invalid port. 2022-01-17 18:36:49 -06:00
91adfb5917 Fix handling invalid ports.
Instead of wrapping around, which is not behaviour any reasonable
user would expect, just use the default port if above 65535.

Disallow connecting on port 0. This port has special meaning and
servers can not listen on it. It is more likely the user just
gave an invalid value to the port field as atoi("invalid") == 0.
2022-01-17 18:36:49 -06:00
9c7109b578 meson: Fix exported functions for python plugin
This fixes loading python plugin in Windows by exporting functions using
python.def file. Otherwise, hexchat_plugin_init symbol error is shown.
2022-01-11 17:26:31 -06:00
d936b653ac Add missing header
https://github.com/hexchat/hexchat/issues/2652#issuecomment-1007015314
2022-01-06 20:36:14 -06:00
d889a8e019 meson: Remove unused wbemcore dependency 2022-01-05 14:28:21 -06:00
66f5968225 Update comment 2021-12-22 12:05:08 -06:00
ba5d79b496 Be smarter about conditionally escaping URIs that are opened
Fixes #2659
2021-12-22 11:50:36 -06:00
7c27dcd524 build: Set G_LOG_DOMAIN 2021-12-22 11:46:55 -06:00
d07e8a8ab2 Remove wallchan command
This command doesn't have many legitimate, non-spam applications and is
easily confused for the similarly named 'wallops'. Moreover, many
netowrks now automatically punish or drop users who message many
channels at the same time, rendering the command mostly useless.

It also is too easy to tab-complete 'wall' into 'wallchan' when you
expect 'wallops' to come up first, which can lead to two very different
functions. If this is to be reintroduced it should be named something
with less similarity to 'wallops' or 'wallchops'.
2021-12-02 14:41:01 -06:00
3ebb2c5eec Make build job names more descriptive (#2657)
Previously every build showed up in the CI as "build".

Update the job names to reflect what they are. For example the Ubuntu
build is now called "ubuntu_build"

Co-authored-by: Patrick <tingping@tingping.se>
2021-12-01 13:07:34 -06:00
bbd60a96ec fish: enable the legacy provider if build against OpenSSL3
OpenSSL 3.0 disables a number of "legacy" algorithms by default, and we
need to enable them manually using their provider system. Note that
explicitly loading a provider will disable the implicit default
provider, which is why we need to load it explicitly.

Closes #2629

Signed-off-by: Simon Chopin <simon.chopin@canonical.com>

V2:
  * use a local OSSL_LIB_CTX to avoid leaking the legacy algorithms
    into the main SSL context.
  * Simplify the fish_init() error paths by calling fish_deinit()
2021-11-30 08:35:04 -06:00
8443755772 Fix timer being locale dependent for decimals
`/timer .1 echo hi` now works in all locales.
2021-11-12 12:44:09 -06:00
f93b13a6a3 Add missing string.h includes
Closes #2652
2021-11-11 10:24:39 -06:00
4f3ef3505a fishlim: Minor test improvements
- Don't have tests repeat themselves, meson has a `--repeat` flag
- Fix a minor leak of a GRand
- Speed up a test
- Increase timeout

This still needs a lot of improvements, it runs at lot of loops within
loops generating random strings that could be optimized. This means
it can take a very long time on some computers.

Closes #2629
2021-11-03 09:59:24 -05:00
b54593e752 Update servlist.c (#2648)
Added irc.irc-nerds.net to the server list
2021-10-30 10:51:46 -05:00
64da6ce1fc flatpak: Update shared-modules 2021-10-26 15:51:53 -05:00
3f099bace2 flatpak: Remove rename-icon from manifest 2021-10-26 11:53:00 -05:00
dac8ace90c Install icons as io.github.Hexchat
This matches our app-id as the desktop-file spec recommends.

This also fixes a bug where our notifications referred to this new
name already.
2021-10-25 15:04:38 -05:00
f42f6af1b9 Adjust parsing of RPL_WHOISSPECIAL to handle missing : for single-word whois messages
This is to support parsing the RPL_WHOISSPECIAL from unrealircd correctly if the whois message is a single word.
2021-10-20 20:48:29 -05:00
9039a5d75b Add -NOOVERRIDE flag to GUI COLOR. (#2644) 2021-10-14 09:44:11 -05:00
aabe3438fa ci: Don't install libproxy-dev 2021-10-07 14:05:47 -05:00
6fd8a8f9bf python: Open all scripts with utf-8 encoding 2021-10-02 09:49:17 -05:00
40399b1cb6 Bump version to 2.16.0 2021-10-01 14:52:09 -05:00
dd6f53f504 Fix user list not tracking mode changes
The `PREFIX` key in `ISUPPORT` (usually) takes the form
`(modes)prefixes` e.g. `(ov)@+`. The current implementation will
therefore set `serv->nick_modes` to a string like `"(ov"` instead
of the desired `"ov"`. This causes the nick list to not properly
update with which users have which prefix modes. Skip over the
initial `'('` so we capture the correct modes and fix that issue.
2021-10-01 14:50:44 -05:00
3f07670b34 win32: Update to OpenSSL 1.1 2021-10-01 13:47:42 -05:00
2985dde7f0 Explicitly set app icon in notifications 2021-10-01 11:56:49 -05:00
8239fbd041 Be a bit less insulting about servers with longer line lengths. 2021-08-24 16:40:54 -05:00
899b4cd3eb Increase the linebuf length to fit a full message including tags. 2021-08-24 16:40:54 -05:00
ef0e670392 Remove some weird guesswork on the 004 numeric. (#2621)
Bahamut and ircu both send 005 MODES and ELIST so this is entirely
unnecessary. The other IRCd checked for here is for a dead network.

While we're editing this code fix HexChat on servers that can only
support one mode at a time (these are mostly gateway servers).
2021-08-23 10:34:13 -05:00
69ce388a87 actions: Add MSYS2 builder 2021-07-15 20:59:27 -05:00
fee86de499 fish: Misc test cleanups 2021-07-15 20:59:19 -05:00
91439f04c0 Fix whitespace issues 2021-07-13 12:30:47 -05:00
c144d0468b Remove libnotify dependency
Instead just talk directly to the service. This fixes *sending*
a notification being blocking IO.
2021-07-13 12:26:34 -05:00
482efae89a actions: Build on Ubuntu 20.04
18.04 requires newer Ninja
2021-07-13 11:33:00 -05:00
cbb0927a7a build: Misc cleanup of options
Cleanup of option names, use features where applicable, and printing
of summary.
2021-07-13 11:26:59 -05:00
25440a07c3 Avoid direct use of libproxy
Since hexchat already depends on GLib, it's better to use GProxyResolver
instead. This might use libproxy, or not, as appropriate.

P.S. This removes a memory safety issue because proxy_list is allocated
using malloc(), not g_malloc(), and therefore using g_strfreev() is
incorrect. The proper way to free the proxy list returned by libproxy
is to use px_proxy_factory_free_proxies() (but nobody does that because
it was added in libproxy 0.4.16, which is somewhat recent).
2021-07-12 11:29:15 -05:00
869a8d7ab3 Fix allowed characters when escaping URIs
Closes #2608
2021-07-10 11:35:35 -05:00
c8536ed50c servlist: Remove freenode
Closes #2604
2021-07-09 19:29:21 -05:00
cfb43bf550 servlist: Add back TURLINet (#2602) 2021-07-02 02:36:29 +00:00
816769af5b Add DigitalIRC to default servlist.c 2021-06-29 13:53:23 -05:00
c5e0b22c55 servlist: Add ICQ-Chat
Closes #2506
2021-06-26 10:51:32 -05:00
c9145a1460 Update servlist.c - Network clean up (#2597)
Added 1 server to Aitvaras
Added 1 server to EFNet
Added 2 servers to chatpat (previously UniBG)
Added DosersNET
Put network list into alphabetical order.
Removed 2 servers from EFNet
Removed 3 servers from Aitvaras
Removed 3 servers from UniBG (now chatpat)
Removed AccessIRC (no longer exists)
Removed BetaChat (no longer exists)
Removed Buddy.IM (no longer exists)
Removed ChatNet (no longer exists)
Removed ChattingAway (no longer exists)
Removed Criten (connects to Rizon)
Removed DeltaPool for having zero connections and channels.
Removed ElectroCode (no longer exists)
Removed GalaxyNet (no longer exists)
Removed GeeksIRC (no longer exists)
Removed IdleMonkeys (no longer exists)
Removed IndirectIRC (no longer exists)
Removed iZ-smart.net (no longer exists)
Removed ObsidianIRC (no longer exists)
Removed PonyChat (no longer exists)
Removed SceneNet (connects to ChatJunkies)
Removed SeilEn.de (no longer exists)
Removed SolidIRC (no longer exists)
Removed StarChat (no longer exists)
Removed TURLINet (no longer exists)
Removed WorldNet (no longer exists)
Renamed DeltaAnime to DaIRC
Renamed Irctoo.net to IRCtoo
Renamed KBFail to Keyboard-Failure
Renamed Krstarica to PIK
Renamed OzNet to OzOrg
Renamed PIRC.PL to pirc.pl
Renamed PTNet.org to PTNet
Renamed UniBG to chatpat
2021-06-26 01:14:42 +00:00
199c03c8c6 Fix parsing +beI lists on InspIRCd. 2021-06-22 09:50:22 -05:00
cdcdeacd63 actions: Remove default value in ubuntu build 2021-06-21 12:48:47 -05:00
28a4726ddc actions: Add flatpak action 2021-06-21 12:48:38 -05:00
6b7d110ced actions: Upload windows artifacts for each arch 2021-06-21 12:48:32 -05:00
d5b4577315 Implement generic support for IRCv3 standard replies. (#2589)
https://ircv3.net/specs/extensions/standard-replies

Co-authored-by: Patrick <tingping@tingping.se>
2021-06-20 23:29:36 +00:00
55e4f1c42e Implement support for strikethrough text.
https://defs.ircdocs.horse/info/formatting.html
2021-06-20 10:39:39 -05:00
08e13a3ac5 Replace identify-msg support with solanum.chat/identify-msg. 2021-06-19 20:16:40 -05:00
f5926fbd23 Consistently set the SSL state in /reconnect.
We need to use a temporary variable here as we're overwriting the
existing server object which may have values set here already.
2021-06-17 19:47:34 -05:00
623d93c6f1 Switch back to using newserver as the default server name. 2021-06-17 19:47:34 -05:00
1f608e600b Require opting out of SSL verification in /server and /reconnect. 2021-06-17 19:47:34 -05:00
747a52aae8 Default new servers to use TLS if built with OpenSSL. 2021-06-17 19:47:34 -05:00
1f5c95d9e9 Always pass a valid URI to gtk_show_uri()
This can fix issues like a crash when invalid characters get passed
through.
2021-06-17 15:22:40 -05:00
09e9d1f749 Place ChanServ notices in the front buffer if the front buffer is on the same network. 2021-06-17 11:03:18 -05:00
333a02d015 Implement support for the IRCv3 UTF8ONLY specification.
https://ircv3.net/specs/extensions/utf8-only
2021-06-01 09:26:48 -05:00
734d888210 python: Fix off by one range
The range goes from 31 to 1 inclusive (#2391).
2021-05-28 19:39:44 -05:00
4fc22a978a Parse the output of the 005 numeric correctly. (#2585)
This implements support for the full 005 numeric syntax including negation and value escapes as defined in draft-hardy-irc-isupport-00. This fixes HexChat on servers that:

- Have unloaded a previously supported feature at runtime (e.g. unloading the monitor module in InspIRCd removing the MONITOR token).
- Have escaped spaces in the network name (see testnet.inspircd.org for an example of this).
- Send a value for a token where HexChat expects none (e.g. INVEX on InspIRCd — the value for this token is optional) or vice versa.
2021-05-28 19:37:50 -05:00
7f8b0a19cf Add ACN IRC Network (#2524)
Website: https://irc.acn.gr
Round-Robin DNS: global.acn.gr
Ports: 6667 - 6697(ssl only).
2021-05-24 13:26:06 -05:00
cdfc3b9ea9 Update servlist.c (#2522)
* Update servlist.c

Added DeltaPool to IRC Networks

* Update servlist.c

Updated to support SASL
2021-05-23 22:46:00 -05:00
076b2c1c73 Merge pull request #1457 from arodland/forgiving-ctcp
Be forgiving of a missing ending CTCP delimiter in a truncated message
2021-05-23 21:19:28 -05:00
7121bb6e82 plugin interface: 💄 2021-05-23 21:17:07 -05:00
da26097aab notification: Implement notification option for channels 2021-05-23 21:17:07 -05:00
e03fab07ed plugin interface: Refactor "flags" option in "channels" list to be more clear with bit operators 2021-05-23 21:17:07 -05:00
0a85d79dff Adding LibertaCasa + TripSit to servlist.c (#2538) 2021-05-23 21:16:39 -05:00
d3545f37cd Change default network to Libera.Chat 2021-05-23 21:15:52 -05:00
ad20708766 Added SimosNap to server list (#2349) 2021-05-24 02:12:20 +00:00
37118a4d2b Implement support for the IRCv3 account-tag specification. (#2572)
Co-authored-by: Patrick <tingping@tingping.se>
2021-05-23 20:53:28 -05:00
6199635e7f Add the official EU server to hackint network (#2495) 2021-05-23 20:47:33 -05:00
c64dda4dea Update ptnet servers (#2205)
Co-authored-by: Elias <elias-m-barreira@telecom.pt>
2021-05-23 20:42:07 -05:00
5310f451f2 ci: fixed python paths 2021-05-23 19:43:24 -05:00
65930492ca ci: fixed Inno Download Plugin download path 2021-05-23 19:43:24 -05:00
04acbdc221 Update github workflows 2021-05-23 19:43:24 -05:00
e2ec2c9ab7 Fixed notifications-winrt compilation error
Both platform.winmd and windows.winmd were unable to find so added the location of each to the compiler.
2021-05-23 19:43:24 -05:00
939ec7a16e Updated Toolset to v142 2021-05-23 19:43:24 -05:00
29e78d3851 Change Inno path property 2021-05-23 19:43:24 -05:00
c06f6f2565 Implement support for the IRCv3 invite-notify specification. (#2574) 2021-05-23 19:32:00 -05:00
e4fd69e3d4 Implement support for the IRCv3 SETNAME specification. (#2571) 2021-05-23 13:12:10 -05:00
f0554b27df Add a workaround for icons not scaling right on HiDPI screens. (#2573) 2021-05-23 13:01:39 -05:00
65edc9ad9a add tilde.chat
https://tilde.chat
2021-05-21 11:05:42 -05:00
a25f238168 Add Libera Chat to network list 2021-05-19 12:08:03 -05:00
90c91d6c9a plugins/lua/lua.c: fix segfault on lua_pop with Lua 5.4.3
Closes #2558

Co-authored-by: "Jan Alexander Steffens (heftig)" <jan.steffens@gmail.com>
Signed-off-by: Mateusz Gozdek <mgozdekof@gmail.com>
2021-04-04 21:17:05 -05:00
090fd29acf python: Fix exception with list_pluginpref()
__decode cannot work (with Python3) because prefs_str has no attribute 'decode'.

Related to https://github.com/hexchat/hexchat/issues/2531
2021-03-07 15:20:58 -05:00
cc04916137 url.c: add gemini & gopher parsing 2021-03-07 11:59:04 -05:00
964ae72fa8 Better handle various ctime() calls failing 2021-03-03 15:39:02 -06:00
87eb728147 docs: fix simple typo, wory -> worry
There is a small typo in src/fe-text/fe-text.c.

Should read `worry` rather than `wory`.
2020-11-22 10:25:50 -06:00
078af20e8b fishlim: Implement correct handling of long and UTF-8 messages 2020-10-16 23:19:10 +02:00
bd3f3fa5f7 fishlim: Remove needless header 2020-10-16 23:19:10 +02:00
df818ad7d9 fishlim: Remove compiler warnings 2020-10-16 23:19:10 +02:00
c7844c775a fishlim: Remove needless functions for tests 2020-10-16 23:19:10 +02:00
4758d3705d fishlim: Fix result 2020-10-16 23:19:10 +02:00
bbbc2aad1b fishlim: Fix cast 2020-10-16 23:19:10 +02:00
7a275812c0 Revert word array length change
It turns out that the rfc sets a limit of 15 arguments and the
server (irccloud) sending that many in ISUPPORT was updated to
split it into multiple lines.
2020-09-21 11:22:50 -07:00
453cb7ca79 Increase max number of words a line can be split into
This may have unintended side-effects but 32 is a very low value
and I was seeing real world bugs being caused by this. Specifically
an ISUPPORT line with more features than this could store.
2020-09-17 15:50:28 -07:00
163608d7fd Use pango_font_metrics_get_height() to calculate font height (#2500) 2020-09-07 18:53:31 +02:00
71eb79fee4 Hide Focus Channel when the selected channel is already focussed
When the channel is focussed, the menu item does nothing so
it isn't useful to have it in the menu.

Fixes: commit c361bdca6a
See-also: https://github.com/hexchat/hexchat/pull/2255#issuecomment-475841824
2020-08-05 18:12:31 +02:00
aec72593f2 SASL EXTERNAL doesn't necessitate a certificate 2020-07-22 10:34:19 -07:00
c5a798beec FiSHLiM: Support for CBC mode + more commands (#2347) 2020-07-13 16:27:27 -07:00
2f376953f3 Add "DarkScience" to default server list. (#2474) 2020-05-31 17:59:06 -07:00
53952feddd Fix parsing of 313
Closes #2472
2020-05-26 16:50:04 -07:00
f9adf88eca Remove 2ch from network list
It split into multiple networks; Both are very small and can't even match our modern guidelines like supporting TLS. I'll just use this as an opportunity to clean up the list a bit.

Closes #2465
2020-05-14 23:15:14 -07:00
82a424fc8a win32: Fix undefined symbol for builds with -with-plugin=false
Windows builds without plugins can use notification-windows.c, which
uses module_load in its notification_backend_init function.

module_load was previously guarded with a USE_PLUGIN ifdef, but we do
need this function for Windows builds even if plugins are disabled.

This fixes a critical build issue for all Windows builds without
plugins.
2020-05-02 20:38:17 -07:00
c2cdf0d2a1 win32: Disable ASLR for Windows debug builds
GDB is usually able to debug executables with ASLR by temporarily
disabling ASLR when running that executable. This is only supported on
Linux. On Windows, GDB cannot debug ASLR executables.

This removes the dynamicbase linker flag on Windows for debug builds in
order to be able to debug that executable later.

Hardening an executable with ASLR is important for release builds, but
for debug builds being able to debug is much more important.
2020-04-19 16:13:18 -07:00
83daed8706 win32: Fix building executables with invalid entrypoints
Windows builds of the GTK frontend use the pie flag to compile
hexchat.exe. Windows needs an explicit entrypoint when compiling with
--pie, otherwise an invalid executable is created.

This sets the entrypoint of the executable on Windows (as it is
currently set in the Visual Studio project files).

This fixes a critical build issue which prevents all Windows builds
using Meson from working.
2020-04-19 16:12:42 -07:00
5d5838e712 win32: Replace include of winuser.h with windows.h
winuser.h should never be included directly. windows.h should be included instead.

This fixes a critical build issue added in c5d47fc which makes all MinGW builds fail.

See #2403.
2020-04-19 03:02:29 -07:00
082f2f8ceb Remove Moznet
Mozilla's Moznet no longer exists. They migrated to Matrix.
2020-04-18 15:05:29 -07:00
7b950eb021 Fixed proxy user/password buffer overflow
By using a dedicated buffer for sending the username and password for the SOCKS5 proxy, there will be no overflow when copying them to the buffer.
And therefore, RFC 1929 is fully supported.
2020-04-11 13:19:31 -07:00
37192a9136 Updated the maximum length of the socks5 user and password to comply to RFC 1929, where both the password and the username length is definied as a maximum of 255 2020-04-11 13:19:31 -07:00
3871fbaacb build: Fix potential undefined variable 2020-03-11 11:13:25 -07:00
5deb695919 build: Better support building against python 3.8+
Closes #2441
2020-03-11 11:08:28 -07:00
bcff9a2ad8 Fetch latest .po files 2020-02-08 11:06:07 -08:00
9c44d7baf4 Avoid prioritising MODE queries for channels with hyphens in their name
If a user has a large number of channels containing hyphens in their
names, the initial MODE queries will have the same high priority as any
PINGs, and so will block the PINGs from being sent, causing the
connection to time out due to a lack of PONGs received.
2020-01-01 16:39:11 -08:00
c361bdca6a Add a channel context menu item to focus channels 2019-12-30 18:14:53 -08:00
c522ccce7f Fix build on FreeBSD 2019-12-22 20:45:16 -08:00
58cdff728d appdata: Add OARS information 2019-12-20 23:19:54 -08:00
bfd6eea98f Bump version to 2.14.3 2019-12-20 23:19:32 -08:00
eeada79a64 build: Fix some meson warnings 2019-12-20 22:24:30 -08:00
202393a77c Follow more modern conventions for USER message
Closes #2399
2019-12-20 22:18:51 -08:00
d9809f2787 Add missing winuser.h include for mingw (#2403)
Without the include gcc will complain about WM_TIMECHANGE as undeclared.
2019-12-16 00:42:31 -08:00
ea2f298a1a readme: Remove build status badges
No longer using Travis for CI and honestly these don't provide much value
2019-12-08 12:56:18 -08:00
7d9f3acfc9 Fix capability negotiation ending before sasl finishes with multi-line cap
Closes #2398
2019-11-24 13:01:48 -08:00
ad5be08a07 Ignore some non-interesting filesystem types
Generally, how much space we have in squashfs, or tmpfs shouldn't
interest us. This becomes more relevant in distros like Ubuntu, where
snaps are a thing, and each snap mounts their own FS in a squashfs that
is always full, thus falsifying the output of sysinfo.
2019-11-13 21:37:21 -08:00
308838da32 Switch to Github Actions for Linux CI 2019-09-20 09:53:13 -07:00
nia
92014628d1 build: Make generated headers a dependency for users of common. 2019-07-17 11:50:16 -07:00
586f089df6 Python: Fix error in hexchat.emit_print when passing time attribute 2019-06-24 07:37:20 -07:00
a67eafc796 Revert "Create FUNDING.yml"
This reverts commit 5382401893.
2019-06-03 22:19:45 -07:00
5382401893 Create FUNDING.yml 2019-06-03 21:43:17 -07:00
8bb768ef93 Fix a typo-error in src/common/hexchat.h:485 "haxchatprefs" -> "hexchatprefs" 2019-05-28 14:33:39 -07:00
ed1d5061a4 Make dcc_ip being a per-server value.
Moved dcc_ip from prefs to sess->server.
2019-05-28 14:33:39 -07:00
468ce821fe Try building with lgtm 2019-05-22 12:41:50 +02:00
87470f30a9 servlist: add hackint irc network
- requires the use of TLS to connect on port 6697
- supports and encourages authentication via SASL PLAIN and EXTERNAL
2019-05-03 14:36:52 -07:00
ba72cc7b6d Update servlist.c
Update servlist.c
2019-04-21 18:33:24 +00:00
c1091c38b8 Extend input box GTK theme workaround to include Yaru
Fixes #2305
2019-02-25 19:13:24 -05:00
804f959a1d Remove : from various trailing parameters (#2301)
Partial fix for #2271 

This isn't an exhaustive list, but it's everything I could find. The bug still exists in the parser though, this is just a workaround for the moment
2019-01-30 19:46:13 -05:00
A_D
7abeb10cf1 python: plugin cleanup and refactor 2019-01-02 18:50:10 -05:00
a5a727122b python: Make sure help() doesn't cause hexchat to hang (#2290)
* Make sure `help()` doesn't cause hexchat to hang

Replace `pydoc.help` with a copy of `pydoc.Helper` with an empty
`StringIO` instead of stdin

* Handle BytesIO vs StringIO on 2.7
2018-12-27 14:46:02 -05:00
f7713a6a64 python: Make the plugins table dynamically sized (#2291)
Adjust the width of the columns depending on the length of the data in
each element
2018-12-26 17:15:25 -05:00
A_D
3ebfa83fdd python: Made sure to set sys.argv if it is not set. fixes #2282 2018-12-26 16:58:46 -05:00
ed55330153 python: Fix console not eating commands 2018-12-05 19:45:30 -05:00
A_D
a2ff661d40 python: Various cffi fixes
- fixed /py exec behaviour
- fixed hexchat.unload_hook() failing when passed a hook id
- fixed get_list() calls in python3
2018-11-09 18:36:59 -05:00
706f9bca82 python: Rewrite with CFFI 2018-11-09 18:36:59 -05:00
6432694455 Fix compilation failure on non-linux, non-darwin, non-windows systems.
'gnu' => Hurd
'gnu/' => all the gnu/* stuff like gnu/kfreebsd

Signed-off-by: Mattia Rizzolo <mattia@mapreri.org>
2018-09-26 13:18:54 -04:00
cf140f3ab0 Use prefix variable in pkgconfig file 2018-09-23 16:58:18 -04:00
c7322f406c build: Silence some Meson warnings and bump requirement to 0.40.0 2018-09-01 16:50:31 -04:00
18eae24acf Fix new stringop-truncation warnings 2018-09-01 16:35:48 -04:00
c092af89a2 sysinfo: Fixup formatting 2018-09-01 13:01:30 -04:00
2a8ab8bb7f sysinfo: Add support for /etc/os-release 2018-09-01 12:51:07 -04:00
8665501c77 Bump version to 2.14.2 2018-08-29 16:41:08 +00:00
7659caada1 win32: Reflect gvsbuild changes 2018-08-29 16:10:21 +00:00
fd47adf595 Fix inconsistent behavior (re)connecting on SSL 2018-08-16 22:06:36 +00:00
cadc51ede9 build: Add with-appdata option
This is mostly useful to avoid a newer gettext dependency
for translating the appdata file but it is also just useless
data for some distros without any app store.

Closes #2219
2018-07-26 09:53:29 -04:00
57478b6575 Fix sending PASS with spaces or starting with :
Closes #2186
Closes #1550
2018-05-08 16:27:18 -04:00
5c5aacd9da Fix another bad translation 2018-04-04 19:21:53 -04:00
93cc105a40 travis: Avoid locale problems 2018-04-04 19:14:59 -04:00
33300630a3 tests: Explicitly open files as utf-8 for travis 2018-04-03 16:38:53 -04:00
fd2167d856 Fix tests on Ubuntu 2018-04-03 16:30:38 -04:00
08fb808ea4 Update translations 2018-04-03 16:14:25 -04:00
c70c1e1896 travis: Run tests 2018-04-03 16:09:25 -04:00
5cd70622aa Validate all translations contain valid text events 2018-04-03 16:08:27 -04:00
5ca767f7f8 Fix plugins on macOS
The switch to the meson build system broke plugins on macOS. GNU libtool
builds shared libraries with ".dylib" and shared modules (plugins) with
the extension ".so", but meson is using ".dylib" for both.

Although overriding the name_suffix for shared_module() in meson is
possible, this would be messy for other platforms as there is no way to
query the default. Therefore it seems like we have to go with ".dylib"
for now on macOS.

However, G_MODULE_SUFFIX is defined to ".so", because glib follows what
GNU libtool does. Therefore define a separate preprocessor macro that
has the correct extension.

See: https://github.com/mesonbuild/meson/issues/1160
2018-03-31 01:29:05 +00:00
111441302c build: perl as a dependency in meson.build
With the switch to meson, the problem previously fixed in #1822 came
back. The build system might pick up the installed hexchat-config.h
instead of using the header in the source directory, as the compiler
arguments would be in the order of "-I${prefix}/include -I..".

It seems that the c_args in meson are always put to the front of the
compiler arguments, in order to be able to override any include paths
from dependencies. However, this was not the intention here, so perl
should also be modeled as a dependency. This ensures that the arguments
with local include directories come first.
2018-03-26 17:09:36 +00:00
ed6f544572 build: Add option to specify path to perl binary 2018-03-23 09:14:45 +00:00
ee85129a9b Deiconify window on tray click. Closes #2136 2018-03-20 11:32:02 +00:00
93f926bf12 build: Re-add support for the legacy perl api
This was accidentally left behind, expose it beind an option as
with the old build system but default to false now. Enough time
has passed and only distros that care about it can enable it.
2018-03-18 11:09:53 -04:00
da56297c5a build: Correctly set plugin licenses 2018-03-17 01:37:46 -04:00
5d8b4719a8 build: Fix id in plugin metainfo files 2018-03-17 01:26:12 -04:00
8a875afad0 build: Add metainfo files for addons 2018-03-17 01:21:50 -04:00
dc483b2342 Remove shift+click to close tab binding
It is an odd binding that conflicts with typical behavior
where shift click selects multiple items and there is
already the middle click shortcut to close tabs quickly.

Closes #918
2018-03-16 20:33:52 -04:00
28a3d42ad1 Bump to 2.14.1 2018-03-13 22:26:31 -04:00
eb942fc274 Revert "xtext: Always use Pango to get correct glyph width on Unix"
This reverts commit d3f1ab7813.

The performance even on Linux is just too poor in many cases.
2018-03-13 21:18:16 -04:00
27acca0f5b fix typo in comment
Signed-off-by: Mattia Rizzolo <mattia@mapreri.org>
2018-03-13 23:38:36 +00:00
ececf2f640 Fix fscanf() usage without size limit
Closes #2137
2018-03-11 19:08:26 -04:00
d72249d91f build: Remove -pie from global ldflags
According to `hardening-check` the cflag is enough for `hexchat`
and this was causing breakage in plugins

Closes #2132
2018-03-10 20:49:35 -05:00
193 changed files with 23015 additions and 22135 deletions

17
.github/workflows/flatpak-build.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Flatpak Build
on: [push, pull_request]
jobs:
flatpak_build:
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-40
options: --privileged
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v3
with:
bundle: hexchat.flatpak
manifest-path: flatpak/io.github.Hexchat.json

41
.github/workflows/msys-build.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: MSYS2 Build
on: [push, pull_request]
jobs:
msys2_build:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v2
- uses: msys2/setup-msys2@v2
with:
install: >-
mingw-w64-x86_64-gcc
mingw-w64-x86_64-pkg-config
mingw-w64-x86_64-python3-cffi
mingw-w64-x86_64-meson
mingw-w64-x86_64-gtk2
mingw-w64-x86_64-gtk-update-icon-cache
mingw-w64-x86_64-luajit
mingw-w64-x86_64-desktop-file-utils
- name: Configure
run: >-
meson build
-Dtext-frontend=true
-Ddbus=disabled
-Dwith-upd=false
-Dwith-perl=false
- name: Build
run: ninja -C build
- name: Test
run: ninja -C build test
- name: Install
run: ninja -C build install

25
.github/workflows/ubuntu-build.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Ubuntu Build
on: [push, pull_request]
jobs:
ubuntu_build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y meson libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libpci-dev libperl-dev libssl-dev python3-dev python3-cffi mono-devel desktop-file-utils
- name: Configure
run: meson build -Dtext=true -Dtheme-manager=true -Dauto_features=enabled
- name: Build
run: ninja -C build
- name: Test
run: ninja -C build test
- name: Install
run: sudo ninja -C build install

72
.github/workflows/windows-build.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: Windows Build
on: [push, pull_request]
jobs:
windows_build:
runs-on: windows-2019
strategy:
matrix:
platform: [x64, win32]
arch: [x64, x86]
exclude:
- platform: x64
arch: x86
- platform: win32
arch: x64
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: |
New-Item -Name "deps" -ItemType "Directory"
Invoke-WebRequest http://files.jrsoftware.org/is/5/innosetup-5.5.9-unicode.exe -OutFile deps\innosetup-unicode.exe
& deps\innosetup-unicode.exe /VERYSILENT | Out-Null
Invoke-WebRequest https://dl.hexchat.net/misc/idpsetup-1.5.1.exe -OutFile deps\idpsetup.exe
& deps\idpsetup.exe /VERYSILENT
Invoke-WebRequest https://dl.hexchat.net/gtk/gtk-${{ matrix.platform }}-2018-08-29-openssl1.1.7z -OutFile deps\gtk-${{ matrix.arch }}.7z
& 7z.exe x deps\gtk-${{ matrix.arch }}.7z -oC:\gtk-build\gtk
Invoke-WebRequest https://dl.hexchat.net/gtk-win32/gendef-20111031.7z -OutFile deps\gendef.7z
& 7z.exe x deps\gendef.7z -oC:\gtk-build
Invoke-WebRequest https://dl.hexchat.net/gtk-win32/WinSparkle-20151011.7z -OutFile deps\WinSparkle.7z
& 7z.exe x deps\WinSparkle.7z -oC:\gtk-build\WinSparkle
Invoke-WebRequest https://dl.hexchat.net/misc/perl/perl-5.20.0-${{ matrix.arch }}.7z -OutFile deps\perl-${{ matrix.arch }}.7z
& 7z.exe x deps\perl-${{ matrix.arch }}.7z -oC:\gtk-build\perl-5.20\${{ matrix.platform }}
New-Item -Path "c:\gtk-build" -Name "python-2.7" -ItemType "Directory"
New-Item -Path "c:\gtk-build" -Name "python-3.8" -ItemType "Directory"
New-Item -Path "c:\gtk-build\python-2.7" -Name "${{ matrix.platform }}" -ItemType "SymbolicLink" -Value "C:/hostedtoolcache/windows/Python/2.7.18/${{ matrix.arch }}"
New-Item -Path "c:\gtk-build\python-3.8" -Name "${{ matrix.platform }}" -ItemType "SymbolicLink" -Value "C:/hostedtoolcache/windows/Python/3.8.10/${{ matrix.arch }}"
C:/hostedtoolcache/windows/Python/3.8.10/${{ matrix.arch }}/python.exe -m pip install cffi
C:/hostedtoolcache/windows/Python/2.7.18/${{ matrix.arch }}/python.exe -m pip install -qq cffi
shell: powershell
- name: Build
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat"
msbuild win32\hexchat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
shell: cmd
- name: Preparing Artifacts
run: |
move ..\hexchat-build\${{ matrix.platform }}\HexChat*.exe .\
move ..\hexchat-build .\
shell: cmd
- uses: actions/upload-artifact@v2
with:
name: Installer ${{ matrix.arch }}
path: HexChat*.exe
- uses: actions/upload-artifact@v2
with:
name: Build Files ${{ matrix.arch }}
path: hexchat-build

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git

5
.lgtm.yml Normal file
View File

@ -0,0 +1,5 @@
extraction:
cpp:
prepare:
packages:
- python3-cffi

View File

@ -1,20 +0,0 @@
sudo: required
services: docker
before_install:
- docker pull ubuntu:16.04
- docker run --privileged --cidfile=/tmp/cid ubuntu:16.04 /bin/sh -c 'apt-get update && apt-get install -y meson/xenial-backports libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libnotify-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev mono-devel desktop-file-utils'
- docker commit `cat /tmp/cid` hexchat/ubuntu-ci
- rm -f /tmp/cid
install:
- docker run -d --privileged --cidfile=/tmp/cid --volume=${PWD}:/opt/hexchat hexchat/ubuntu-ci /bin/systemd --system
script:
- docker exec `cat /tmp/cid` /bin/sh -c 'meson /opt/hexchat /opt/hexchat-build -Dwith-text=true -Dwith-theme-manager=true && ninja -C /opt/hexchat-build install'
after_script:
- docker kill `cat /tmp/cid`
notifications:
irc:
channels: "chat.freenode.net#hexchat-devel"
template: "Build %{build_url} (%{commit} in %{branch}) by %{author}: %{message}"
on_success: change
matrix:
fast_finish: true

View File

@ -1,9 +1,11 @@
icondir = join_paths(get_option('datadir'), 'icons/hicolor')
install_data(
'hexchat.png',
rename: 'io.github.Hexchat.png',
install_dir: join_paths(icondir, '48x48/apps')
)
install_data(
'hexchat.svg',
rename: 'io.github.Hexchat.svg',
install_dir: join_paths(icondir, 'scalable/apps')
)

View File

@ -1,11 +1,11 @@
if get_option('with-plugin')
if get_option('plugin')
subdir('pkgconfig')
endif
if get_option('with-gtk')
if get_option('gtk-frontend')
subdir('icons')
subdir('misc')
subdir('man')
elif get_option('with-theme-manager')
elif get_option('theme-manager')
subdir('misc')
endif

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="addon">
<id>io.github.Hexchat.Plugin.@NAME@</id>
<extends>io.github.Hexchat</extends>
<name>@NAME@ Plugin</name>
<summary>@SUMMARY@</summary>
<url type="homepage">https://hexchat.github.io/</url>
<project_license>@LICENSE@</project_license>
<metadata_license>CC0-1.0</metadata_license>
<update_contact>tingping_AT_fedoraproject.org</update_contact>
</component>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>io.github.Hexchat.desktop</id>
<component type="desktop-application">
<id>io.github.Hexchat</id>
<name>HexChat</name>
<launchable type="desktop-id">io.github.Hexchat.desktop</launchable>
<developer_name>HexChat</developer_name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-2.0+</project_license>
@ -13,7 +14,6 @@
</description>
<url type="homepage">http://hexchat.github.io</url>
<url type="bugtracker">https://github.com/hexchat/hexchat</url>
<url type="translate">https://www.transifex.com/hexchat/hexchat</url>
<url type="donation">https://goo.gl/jESZvU</url>
<url type="help">https://hexchat.readthedocs.io/en/latest/</url>
<screenshots>
@ -27,6 +27,67 @@
<id>hexchat.desktop</id>
</provides>
<releases>
<release date="2022-02-12" version="2.16.1">
<description>
<p>This is a minor release with mostly bug-fixes:</p>
<ul>
<li>Add `-NOOVERRIDE` flag to the `GUI COLOR` command</li>
<li>Add `-q` (quiet) flag to the `EXECWRITE` command</li>
<li>Rename installed icon to match app-id (Fixes notification icon)</li>
<li>Fix escaping already escaped URLs when opening them</li>
<li>Fix Python scripts not being opened as UTF-8</li>
<li>Fix `TIMER` command supporting decimals regardless of locale</li>
</ul>
</description>
</release>
<release date="2021-10-01" version="2.16.0">
<description>
<p>This is a feature release:</p>
<ul>
<li>Add support for IRCv3 SETNAME, invite-notify, account-tag, standard replies, and UTF8ONLY</li>
<li>Add support for strikethrough formatting</li>
<li>Fix text clipping issues by respecting font line height</li>
<li>Fix URLs not being escaped when opened</li>
<li>Fix possible hang when showing notifications</li>
<li>Print ChanServ notices in the front tab by default</li>
<li>Update network list</li>
<li>python: Rewrite plugin improving memory usage and compatibility</li>
<li>fishlim: Add support for CBC and other improvements</li>
</ul>
</description>
</release>
<release date="2019-12-20" version="2.14.3">
<description>
<p>This is a bug-fix release:</p>
<ul>
<li>Fix various incorrect parsing of IRC messages relating to trailing parameters</li>
<li>Fix SASL negotiation combined with multi-line cap</li>
<li>Fix input box theming with Yaru theme</li>
<li>python: Work around Python 3.7 regression causing crash on unload</li>
<li>sysinfo: Add support for /etc/os-release</li>
<li>sysinfo: Ignore irrelevant mounts when calculating storage size</li>
</ul>
</description>
</release>
<release date="2018-08-29" version="2.14.2">
<description>
<p>This is a minor release:</p>
<ul>
<li>Remove shift+click to close tab binding</li>
<li>Always unminimize when opening from tray</li>
<li>Fix some translations containing invalid text events</li>
<li>Fix sending server passwords starting with ":"</li>
</ul>
</description>
</release>
<release date="2018-03-13" version="2.14.1">
<description>
<p>This is a very minor bug-fix release:</p>
<ul>
<li>Fix performance regression</li>
</ul>
</description>
</release>
<release date="2018-03-10" version="2.14.0">
<description>
<p>This is largely a bug fix release though it has some large behind the scenes changes:</p>
@ -83,5 +144,8 @@
<kudo>HiDpiIcon</kudo>
<kudo>Notifications</kudo>
</kudos>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<update_contact>tingping_at_fedoraproject.org</update_contact>
</component>

View File

@ -4,7 +4,7 @@ GenericName=IRC Client
Comment=Chat with other people online
Keywords=IM;Chat;
Exec=@exec_command@
Icon=hexchat
Icon=io.github.Hexchat
Terminal=false
Type=Application
Categories=GTK;Network;IRCClient;

View File

@ -1,24 +1,27 @@
appdir = join_paths(get_option('datadir'), 'applications')
metainfodir = join_paths(get_option('datadir'), 'metainfo')
desktop_utils = find_program('desktop-file-validate', required: false)
if get_option('with-gtk')
hexchat_appdata = i18n.merge_file(
input: 'io.github.Hexchat.appdata.xml.in',
output: 'io.github.Hexchat.appdata.xml',
po_dir: '../../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'metainfo')
)
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found()
test('Validate io.github.Hexchat.appdata.xml', appstream_util,
args: ['validate-relax', hexchat_appdata]
if get_option('gtk-frontend')
if get_option('install-appdata')
hexchat_appdata = i18n.merge_file(
input: 'io.github.Hexchat.appdata.xml.in',
output: 'io.github.Hexchat.appdata.xml',
po_dir: '../../po',
install: true,
install_dir: metainfodir
)
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found()
test('Validate io.github.Hexchat.appdata.xml', appstream_util,
args: ['validate-relax', hexchat_appdata]
)
endif
endif
desktop_conf = configuration_data()
if get_option('with-dbus')
if dbus_glib_dep.found()
desktop_conf.set('exec_command', 'hexchat --existing %U')
else
desktop_conf.set('exec_command', 'hexchat %U')
@ -46,7 +49,7 @@ if get_option('with-gtk')
endif
endif
if get_option('with-theme-manager')
if get_option('theme-manager')
htm_desktop = i18n.merge_file(
input: 'io.github.Hexchat.ThemeManager.desktop.in',
output: 'io.github.Hexchat.ThemeManager.desktop',
@ -66,3 +69,59 @@ if get_option('with-theme-manager')
install_dir: join_paths(get_option('datadir'), 'mime/packages')
)
endif
if get_option('plugin')
plugin_metainfo = []
# FIXME: These should all get translated somewhere
if get_option('with-checksum')
plugin_metainfo += [
['Checksum', 'Calculates a checksum for all sent and recieved DCC files', 'MIT']
]
endif
if get_option('with-fishlim')
plugin_metainfo += [
['Fishlim', 'Allows setting a key for encrypted conversations', 'MIT AND GPL-2.0+']
]
endif
if get_option('with-lua') != 'false'
plugin_metainfo += [
['Lua', 'Provides a scripting interface in Lua', 'MIT']
]
endif
if get_option('with-perl') != 'false'
plugin_metainfo += [
['Perl', 'Provides a scripting interface in Perl', 'GPL-2.0+']
]
endif
if get_option('with-python') != 'false'
plugin_metainfo += [
['Python', 'Provides a scripting interface in Python', 'GPL-2.0+']
]
endif
if get_option('with-sysinfo')
plugin_metainfo += [
['Sysinfo', 'Adds command to display system information', 'GPL-2.0+']
]
endif
foreach metainfo : plugin_metainfo
name = metainfo[0]
conf = configuration_data()
conf.set('NAME', name)
conf.set('SUMMARY', metainfo[1])
conf.set('LICENSE', metainfo[2])
configure_file(
input: 'io.github.Hexchat.Plugin.metainfo.xml.in',
output: 'io.github.Hexchat.Plugin.@0@.metainfo.xml'.format(name),
configuration: conf,
install_dir: get_option('install-plugin-metainfo') ? metainfodir : '',
)
endforeach
endif

View File

@ -2,8 +2,8 @@ pkg_conf = configuration_data()
prefix = get_option('prefix')
pkg_conf.set('prefix', prefix)
pkg_conf.set('VERSION', meson.project_version())
pkg_conf.set('hexchatlibdir', join_paths(prefix, plugindir))
pkg_conf.set('includedir', join_paths(prefix, get_option('includedir')))
pkg_conf.set('hexchatlibdir', join_paths('${prefix}', plugindir))
pkg_conf.set('includedir', join_paths('${prefix}', get_option('includedir')))
configure_file(
input: 'hexchat-plugin.pc.in',

View File

@ -0,0 +1,25 @@
From 918503d57c6740d20be68a6717158673a2a8b25f Mon Sep 17 00:00:00 2001
From: Patrick Griffis <tingping@tingping.se>
Date: Sat, 17 Mar 2018 05:57:49 -0400
Subject: [PATCH] Support loading Flatpak extensions
---
src/common/plugin.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/common/plugin.c b/src/common/plugin.c
index 3ad3c558..6addf962 100644
--- a/src/common/plugin.c
+++ b/src/common/plugin.c
@@ -450,6 +450,8 @@ plugin_auto_load (session *sess)
lib_dir = plugin_get_libdir ();
sub_dir = g_build_filename (get_xdir (), "addons", NULL);
+ for_files ("/app/extensions/lib/hexchat/plugins", "*.so", plugin_auto_load_cb);
+
#ifdef WIN32
/* a long list of bundled plugins that should be loaded automatically,
* user plugins should go to <config>, leave Program Files alone! */
--
2.14.3

View File

@ -0,0 +1,78 @@
{
"app-id": "io.github.Hexchat",
"branch": "stable",
"runtime": "org.gnome.Platform",
"runtime-version": "40",
"sdk": "org.gnome.Sdk",
"command": "hexchat",
"finish-args": [
"--share=ipc",
"--socket=x11",
"--share=network",
"--socket=pulseaudio",
"--filesystem=xdg-download",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.mpris.MediaPlayer2.*"
],
"add-extensions": {
"io.github.Hexchat.Plugin": {
"version": "20.08",
"directory": "extensions",
"add-ld-path": "lib",
"merge-dirs": "lib/hexchat/plugins",
"subdirectories": true,
"no-autodownload": true,
"autodelete": true
}
},
"modules": [
"shared-modules/gtk2/gtk2.json",
"shared-modules/gtk2/gtk2-common-themes.json",
"shared-modules/dbus-glib/dbus-glib.json",
"shared-modules/lua5.3/lua-5.3.5.json",
"shared-modules/libcanberra/libcanberra.json",
"python3-cffi.json",
{
"name": "lgi",
"buildsystem": "meson",
"sources": [
{
"type": "git",
"url": "https://github.com/pavouk/lgi.git",
"commit": "95418635aa8151a516d43166227ea2b9d4c4403f"
}
]
},
{
"name": "hexchat",
"buildsystem": "meson",
"config-opts": [
"--buildtype=release",
"-Ddbus-service-use-appid=true",
"-Dwith-perl=false",
"-Dwith-lua=lua"
],
"build-options": {
"cflags": "-Wno-error=missing-include-dirs"
},
"cleanup": [
"/share/man"
],
"post-install": [
"install -d /app/extensions"
],
"sources": [
{
"type": "dir",
"path": ".."
},
{
"type": "patch",
"path": "Load-plugins-from-Flatpak-extensions.patch"
}
]
}
]
}

19
flatpak/python3-cffi.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "python3-cffi",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"cffi\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/0f/86/e19659527668d70be91d0369aeaa055b4eb396b0f387a4f92293a20035bd/pycparser-2.20.tar.gz",
"sha256": "2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/a8/20/025f59f929bbcaa579704f443a438135918484fffaacfaddba776b374563/cffi-1.14.5.tar.gz",
"sha256": "fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
}
]
}

View File

@ -1,6 +1,6 @@
project('hexchat', 'c',
version: '2.14.0',
meson_version: '>= 0.38.0',
version: '2.16.1',
meson_version: '>= 0.47.0',
default_options: [
'c_std=gnu89',
'buildtype=debugoptimized',
@ -15,12 +15,17 @@ cc = meson.get_compiler('c')
libgio_dep = dependency('gio-2.0', version: '>= 2.34.0')
libgmodule_dep = dependency('gmodule-2.0')
libcanberra_dep = dependency('libcanberra', version: '>= 0.22',
required: get_option('libcanberra'))
dbus_glib_dep = dependency('dbus-glib-1', required: get_option('dbus'))
global_deps = []
if cc.get_id() == 'msvc'
libssl_dep = cc.find_library('libeay32')
libssl_dep = cc.find_library('libssl')
else
libssl_dep = dependency('openssl', version: '>= 0.9.8',
required: get_option('with-ssl'))
required: get_option('tls'))
endif
config_h = configuration_data()
@ -29,14 +34,14 @@ config_h.set_quoted('PACKAGE_NAME', meson.project_name())
config_h.set_quoted('GETTEXT_PACKAGE', 'hexchat')
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
get_option('datadir'), 'locale'))
config_h.set_quoted('G_LOG_DOMAIN', 'hexchat')
config_h.set10('ENABLE_NLS', true)
# Optional features
config_h.set('USE_OPENSSL', get_option('with-ssl'))
config_h.set('USE_LIBPROXY', get_option('with-libproxy'))
config_h.set('USE_LIBCANBERRA', get_option('with-libcanberra'))
config_h.set('USE_DBUS', get_option('with-dbus'))
config_h.set('USE_PLUGIN', get_option('with-plugin'))
config_h.set('USE_OPENSSL', libssl_dep.found())
config_h.set('USE_LIBCANBERRA', libcanberra_dep.found())
config_h.set('USE_DBUS', dbus_glib_dep.found())
config_h.set('USE_PLUGIN', get_option('plugin'))
config_h.set('G_DISABLE_SINGLE_INCLUDES', true)
config_h.set('GTK_DISABLE_DEPRECATED', true)
@ -49,6 +54,10 @@ config_h.set('GLIB_VERSION_MIN_REQUIRED', 'GLIB_VERSION_2_34')
config_h.set('HAVE_MEMRCHR', cc.has_function('memrchr'))
config_h.set('HAVE_STRINGS_H', cc.has_header('strings.h'))
config_h.set_quoted('HEXCHATLIBDIR',
join_paths(get_option('prefix'), get_option('libdir'), 'hexchat/plugins')
)
if libssl_dep.found()
config_h.set('HAVE_X509_GET_SIGNATURE_NID',
cc.has_function('X509_get_signature_nid', dependencies: libssl_dep)
@ -87,8 +96,6 @@ endif
global_cflags = []
test_cflags = [
'-pipe',
'-fPIE',
'-funsigned-char',
'-Wno-conversion',
'-Wno-pointer-sign',
@ -131,20 +138,27 @@ global_ldflags = []
test_ldflags = [
'-Wl,-z,relro',
'-Wl,-z,now',
'-Wl,-pie',
# mingw
'-Wl,--dynamicbase',
'-Wl,--nxcompat',
]
if not (host_machine.system() == 'windows' and get_option('debug'))
test_ldflags += '-Wl,--dynamicbase'
endif
foreach ldflag : test_ldflags
if cc.has_argument(ldflag) and cc.links('int main (void) { return 0; }', args: ldflag)
if meson.version().version_compare('>= 0.46.0')
has_arg = cc.has_link_argument(ldflag)
else
has_arg = cc.has_argument(ldflag)
endif
if has_arg and cc.links('int main (void) { return 0; }', args: ldflag)
global_ldflags += ldflag
endif
endforeach
add_project_link_arguments(global_ldflags, language: 'c')
subdir('src')
if get_option('with-plugin')
if get_option('plugin')
subdir('plugins')
endif
if cc.get_id() != 'msvc'
@ -152,6 +166,32 @@ if cc.get_id() != 'msvc'
subdir('po') # FIXME: build xgettext
meson.add_install_script('meson_post_install.py',
'@0@'.format(get_option('with-theme-manager'))
'@0@'.format(get_option('theme-manager'))
)
endif
if meson.version().version_compare('>= 0.53.0')
summary({
'prefix': get_option('prefix'),
'bindir': get_option('bindir'),
'libdir': get_option('libdir'),
'datadir': get_option('datadir'),
}, section: 'Directories')
summary({
'TLS (openssl)': libssl_dep.found(),
'Plugin Support': get_option('plugin'),
'DBus Support': dbus_glib_dep.found(),
'libcanberra': libcanberra_dep.found(),
}, section: 'Features')
summary({
'Lua': get_option('with-lua'),
'Python': get_option('with-python'),
'Perl': get_option('with-perl'),
'Perl Legacy API': get_option('with-perl-legacy-api'),
'FiSH': get_option('with-fishlim'),
'Sysinfo': get_option('with-sysinfo'),
'DCC Checksum': get_option('with-checksum'),
}, section: 'Plugins')
endif

View File

@ -1,33 +1,38 @@
option('with-gtk', type: 'boolean',
# Applications
option('gtk-frontend', type: 'boolean',
description: 'Main graphical interface'
)
option('with-text', type: 'boolean', value: false,
option('text-frontend', type: 'boolean', value: false,
description: 'Text interface (not generally useful)'
)
option('with-ssl', type: 'boolean',
description: 'Support for TLS connections, requires openssl'
)
option('with-plugin', type: 'boolean',
description: 'Support for loadable plugins'
)
option('with-dbus', type: 'boolean',
description: 'Support used for single-instance and scripting interface, Unix only'
)
option('with-libproxy', type: 'boolean',
description: 'Support for getting proxy information, Unix only'
)
option('with-libnotify', type: 'boolean',
description: 'Support for freedesktop notifications, Unix only'
)
option('with-libcanberra', type: 'boolean',
description: 'Support for sound alerts, Unix only'
)
option('with-theme-manager', type: 'boolean', value: false,
option('theme-manager', type: 'boolean', value: false,
description: 'Utility to help manage themes, requires mono/.net'
)
# Features
option('tls', type: 'feature', value: 'enabled',
description: 'Support for TLS connections, requires openssl'
)
option('plugin', type: 'boolean',
description: 'Support for loadable plugins'
)
option('dbus', type: 'feature', value: 'auto',
description: 'Support used for single-instance and scripting interface, Unix only'
)
option('libcanberra', type: 'feature', value: 'auto',
description: 'Support for sound alerts, Unix only'
)
# Install options
option('dbus-service-use-appid', type: 'boolean', value: false,
description: 'Rename dbus service to match app-id, required for Flatpak'
)
option('install-appdata', type: 'boolean',
description: 'Install appdata files for app stores'
)
option('install-plugin-metainfo', type: 'boolean', value: false,
description: 'Installs metainfo files for enabled plugins, useful when distros create split packages'
)
# Plugins
option('with-checksum', type: 'boolean',
@ -42,8 +47,8 @@ option('with-fishlim', type: 'boolean',
option('with-lua', type: 'string', value: 'luajit',
description: 'Lua scripting plugin, value is pkg-config name to use or "false"'
)
option('with-perl', type: 'boolean',
description: 'Perl scripting plugin'
option('with-perl', type: 'string', value: 'perl',
description: 'Perl scripting plugin, value is path to perl executable or "false"'
)
option('with-python', type: 'string', value: 'python3',
description: 'Python scripting plugin. value is pkg-config name to use or "false"'
@ -57,3 +62,6 @@ option('with-upd', type: 'boolean',
option('with-winamp', type: 'boolean',
description: 'Winamp plugin, Windows only'
)
option('with-perl-legacy-api', type: 'boolean', value: false,
description: 'Enables the legacy IRC perl module for compatibility with old scripts'
)

View File

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

View File

@ -3,4 +3,5 @@ shared_module('checksum', 'checksum.c',
install: true,
install_dir: plugindir,
name_prefix: '',
vs_module_defs: 'checksum.def',
)

View File

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

View File

@ -1,5 +1,6 @@
shared_module('exec', 'exec.c',
dependencies: hexchat_plugin_dep,
install: true,
install_dir: plugindir
install_dir: plugindir,
vs_module_defs: 'exec.def',
)

View File

@ -1,6 +1,7 @@
/*
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -27,15 +28,22 @@
#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
#endif
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/blowfish.h>
#include <openssl/rand.h>
#include "keystore.h"
#include "fish.h"
#include "utils.h"
#define IB 64
static const char fish_base64[64] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
/* rfc 2812; 512 - CR-LF = 510 */
static const int MAX_COMMAND_LENGTH = 510;
static const char fish_base64[] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
static const signed char fish_unbase64[256] = {
IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB,
IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB,
@ -53,6 +61,12 @@ static const signed char fish_unbase64[256] = {
27,28,29,30,31,32,33,34, 35,36,37,IB,IB,IB,IB,IB,
};
/**
* Convert Int to 4 Bytes (Big-endian)
*
* @param int source
* @param char* dest
*/
#define GET_BYTES(dest, source) do { \
*((dest)++) = ((source) >> 24) & 0xFF; \
*((dest)++) = ((source) >> 16) & 0xFF; \
@ -60,135 +74,488 @@ static const signed char fish_unbase64[256] = {
*((dest)++) = (source) & 0xFF; \
} while (0);
/**
* Convert 4 Bytes to Int (Big-endian)
*
* @param char* source
* @param int dest
*/
#define GET_LONG(dest, source) do { \
dest = ((uint8_t)*((source)++) << 24); \
dest |= ((uint8_t)*((source)++) << 16); \
dest |= ((uint8_t)*((source)++) << 8); \
dest |= (uint8_t)*((source)++); \
} while (0);
char *fish_encrypt(const char *key, size_t keylen, const char *message) {
BF_KEY bfkey;
size_t messagelen;
size_t i;
int j;
char *encrypted;
char *end;
unsigned char bit;
unsigned char word;
unsigned char d;
BF_set_key(&bfkey, keylen, (const unsigned char*)key);
messagelen = strlen(message);
if (messagelen == 0) return NULL;
encrypted = g_malloc(((messagelen - 1) / 8) * 12 + 12 + 1); /* each 8-byte block becomes 12 bytes */
end = encrypted;
while (*message) {
/* Read 8 bytes (a Blowfish block) */
BF_LONG binary[2] = { 0, 0 };
unsigned char c;
for (i = 0; i < 8; i++) {
c = message[i];
binary[i >> 2] |= c << 8*(3 - (i&3));
if (c == '\0') break;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
#include <openssl/provider.h>
static OSSL_PROVIDER *legacy_provider;
static OSSL_PROVIDER *default_provider;
static OSSL_LIB_CTX *ossl_ctx;
#endif
int fish_init(void)
{
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
ossl_ctx = OSSL_LIB_CTX_new();
if (!ossl_ctx)
return 0;
legacy_provider = OSSL_PROVIDER_load(ossl_ctx, "legacy");
if (!legacy_provider) {
fish_deinit();
return 0;
}
default_provider = OSSL_PROVIDER_load(ossl_ctx, "default");
if (!default_provider) {
fish_deinit();
return 0;
}
#endif
return 1;
}
void fish_deinit(void)
{
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
if (legacy_provider) {
OSSL_PROVIDER_unload(legacy_provider);
legacy_provider = NULL;
}
if (default_provider) {
OSSL_PROVIDER_unload(default_provider);
default_provider = NULL;
}
if (ossl_ctx) {
OSSL_LIB_CTX_free(ossl_ctx);
ossl_ctx = NULL;
}
#endif
}
/**
* Encode ECB FiSH Base64
*
* @param [in] message Bytes to encode
* @param [in] message_len Size of bytes to encode
* @return Array of char with encoded string
*/
char *fish_base64_encode(const char *message, size_t message_len) {
BF_LONG left = 0, right = 0;
int i, j;
char *encoded = NULL;
char *end = NULL;
char *msg = NULL;
if (message_len == 0)
return NULL;
/* Each 8-byte block becomes 12 bytes (fish base64 format) and add 1 byte for \0 */
encoded = g_malloc(((message_len - 1) / 8) * 12 + 12 + 1);
end = encoded;
/* Iterate over each 8-byte block (Blowfish block size) */
for (j = 0; j < message_len; j += 8) {
msg = (char *) message;
/* Set left and right longs */
GET_LONG(left, msg);
GET_LONG(right, msg);
for (i = 0; i < 6; ++i) {
*end++ = fish_base64[right & 0x3fu];
right = (right >> 6u);
}
for (i = 0; i < 6; ++i) {
*end++ = fish_base64[left & 0x3fu];
left = (left >> 6u);
}
/* The previous for'd ensure fill all bytes of encoded, we don't need will with zeros */
message += 8;
/* Encrypt block */
BF_encrypt(binary, &bfkey);
/* Emit FiSH-BASE64 */
bit = 0;
word = 1;
for (j = 0; j < 12; j++) {
d = fish_base64[(binary[word] >> bit) & 63];
*(end++) = d;
bit += 6;
if (j == 5) {
bit = 0;
word = 0;
}
}
/* Stop if a null terminator was found */
if (c == '\0') break;
}
*end = '\0';
return encrypted;
}
char *fish_decrypt(const char *key, size_t keylen, const char *data) {
BF_KEY bfkey;
size_t i;
char *decrypted;
char *end;
unsigned char bit;
unsigned char word;
unsigned char d;
BF_set_key(&bfkey, keylen, (const unsigned char*)key);
decrypted = g_malloc(strlen(data) + 1);
end = decrypted;
while (*data) {
/* Convert from FiSH-BASE64 */
BF_LONG binary[2] = { 0, 0 };
bit = 0;
word = 1;
for (i = 0; i < 12; i++) {
d = fish_unbase64[(const unsigned char)*(data++)];
if (d == IB) goto decrypt_end;
binary[word] |= (unsigned long)d << bit;
bit += 6;
if (i == 5) {
bit = 0;
word = 0;
}
}
/* Decrypt block */
BF_decrypt(binary, &bfkey);
/* Copy to buffer */
GET_BYTES(end, binary[0]);
GET_BYTES(end, binary[1]);
}
decrypt_end:
*end = '\0';
return decrypted;
return encoded;
}
/**
* Encrypts a message (see fish_decrypt). The key is searched for in the
* key store.
* Decode ECB FiSH Base64
*
* @param [in] message Base64 encoded string
* @param [out] final_len Real length of message
* @return Array of char with decoded message
*/
char *fish_encrypt_for_nick(const char *nick, const char *data) {
char *key;
char *encrypted;
char *fish_base64_decode(const char *message, size_t *final_len) {
BF_LONG left, right;
int i;
char *bytes = NULL;
char *msg = NULL;
char *byt = NULL;
size_t message_len;
/* Look for key */
key = keystore_get_key(nick);
if (!key) return NULL;
/* Encrypt */
encrypted = fish_encrypt(key, strlen(key), data);
g_free(key);
return encrypted;
message_len = strlen(message);
/* Ensure blocks of 12 bytes each one and valid characters */
if (message_len == 0 || message_len % 12 != 0 || strspn(message, fish_base64) != message_len)
return NULL;
/* Each 12 bytes becomes 8-byte block and add 1 byte for \0 */
*final_len = ((message_len - 1) / 12) * 8 + 8 + 1;
(*final_len)--; /* We support binary data */
bytes = (char *) g_malloc0(*final_len);
byt = bytes;
msg = (char *) message;
while (*msg) {
right = 0;
left = 0;
for (i = 0; i < 6; i++) right |= (uint8_t) fish_unbase64[(int)*msg++] << (i * 6u);
for (i = 0; i < 6; i++) left |= (uint8_t) fish_unbase64[(int)*msg++] << (i * 6u);
GET_BYTES(byt, left);
GET_BYTES(byt, right);
}
return bytes;
}
/**
* Decrypts a message (see fish_decrypt). The key is searched for in the
* key store.
* Encrypt or decrypt data with Blowfish cipher, support binary data.
*
* Good documentation for EVP:
*
* - https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption
*
* - https://stackoverflow.com/questions/5727646/what-is-the-length-parameter-of-aes-evp-decrypt
*
* - https://stackoverflow.com/questions/26345175/correct-way-to-free-allocate-the-context-in-the-openssl
*
* - https://stackoverflow.com/questions/29874150/working-with-evp-and-openssl-coding-in-c
*
* @param [in] plaintext Bytes to encrypt or decrypt
* @param [in] plaintext_len Size of plaintext
* @param [in] key Bytes of key
* @param [in] keylen Size of key
* @param [in] encode 1 or encrypt 0 for decrypt
* @param [in] mode EVP_CIPH_ECB_MODE or EVP_CIPH_CBC_MODE
* @param [out] ciphertext_len The bytes written
* @return Array of char with data encrypted or decrypted
*/
char *fish_decrypt_from_nick(const char *nick, const char *data) {
char *fish_cipher(const char *plaintext, size_t plaintext_len, const char *key, size_t keylen, int encode, int mode, size_t *ciphertext_len) {
EVP_CIPHER_CTX *ctx;
EVP_CIPHER *cipher = NULL;
int bytes_written = 0;
unsigned char *ciphertext = NULL;
unsigned char *iv_ciphertext = NULL;
unsigned char *iv = NULL;
size_t block_size = 0;
*ciphertext_len = 0;
if (plaintext_len == 0 || keylen == 0 || encode < 0 || encode > 1)
return NULL;
block_size = plaintext_len;
if (mode == EVP_CIPH_CBC_MODE) {
if (encode == 1) {
iv = (unsigned char *) g_malloc0(8);
RAND_bytes(iv, 8);
} else {
if (plaintext_len <= 8) /* IV + DATA */
return NULL;
iv = (unsigned char *) plaintext;
block_size -= 8;
plaintext += 8;
plaintext_len -= 8;
}
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
cipher = EVP_CIPHER_fetch(ossl_ctx, "BF-CBC", NULL);
#else
cipher = (EVP_CIPHER *) EVP_bf_cbc();
#endif
} else if (mode == EVP_CIPH_ECB_MODE) {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
cipher = EVP_CIPHER_fetch(ossl_ctx, "BF-ECB", NULL);
#else
cipher = (EVP_CIPHER *) EVP_bf_ecb();
#endif
}
/* Zero Padding */
if (block_size % 8 != 0) {
block_size = block_size + 8 - (block_size % 8);
}
ciphertext = (unsigned char *) g_malloc0(block_size);
memcpy(ciphertext, plaintext, plaintext_len);
/* Create and initialise the context */
if (!(ctx = EVP_CIPHER_CTX_new()))
return NULL;
/* Initialise the cipher operation only with mode */
if (!EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encode))
return NULL;
/* Set custom key length */
if (!EVP_CIPHER_CTX_set_key_length(ctx, keylen))
return NULL;
/* Finish the initiation the cipher operation */
if (1 != EVP_CipherInit_ex(ctx, NULL, NULL, (const unsigned char *) key, iv, encode))
return NULL;
/* We will manage this */
EVP_CIPHER_CTX_set_padding(ctx, 0);
/* Do cipher operation */
if (1 != EVP_CipherUpdate(ctx, ciphertext, &bytes_written, ciphertext, block_size))
return NULL;
*ciphertext_len = bytes_written;
/* Finalise the cipher. Further ciphertext bytes may be written at this stage */
if (1 != EVP_CipherFinal_ex(ctx, ciphertext + bytes_written, &bytes_written))
return NULL;
*ciphertext_len += bytes_written;
/* Clean up */
EVP_CIPHER_CTX_free(ctx);
if (mode == EVP_CIPH_CBC_MODE && encode == 1) {
/* Join IV + DATA */
iv_ciphertext = g_malloc0(8 + *ciphertext_len);
memcpy(iv_ciphertext, iv, 8);
memcpy(&iv_ciphertext[8], ciphertext, *ciphertext_len);
*ciphertext_len += 8;
g_free(ciphertext);
g_free(iv);
return (char *) iv_ciphertext;
} else {
return (char *) ciphertext;
}
}
/**
* Return a fish or standard Base64 encoded string with data encrypted
* is binary safe
*
* @param [in] key Bytes of key
* @param [in] keylen Size of key
* @param [in] message Bytes to encrypt
* @param [in] message_len Size of message
* @param [in] mode Chiper mode
* @return Array of char with data encrypted
*/
char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode) {
size_t ciphertext_len = 0;
char *ciphertext = NULL;
char *b64 = NULL;
if (keylen == 0 || message_len == 0)
return NULL;
ciphertext = fish_cipher(message, message_len, key, keylen, 1, mode, &ciphertext_len);
if (ciphertext == NULL || ciphertext_len == 0)
return NULL;
switch (mode) {
case FISH_CBC_MODE:
b64 = g_base64_encode((const unsigned char *) ciphertext, ciphertext_len);
break;
case FISH_ECB_MODE:
b64 = fish_base64_encode((const char *) ciphertext, ciphertext_len);
}
g_free(ciphertext);
if (b64 == NULL)
return NULL;
return b64;
}
/**
* Return an array of bytes with data decrypted
* is binary safe
*
* @param [in] key Bytes of key
* @param [in] keylen Size of key
* @param [in] data Fish or standard Base64 encoded string
* @param [in] mode Chiper mode
* @param [out] final_len Length of returned array
* @return Array of char with data decrypted
*/
char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len) {
size_t ciphertext_len = 0;
char *ciphertext = NULL;
char *plaintext = NULL;
*final_len = 0;
if (keylen == 0 || strlen(data) == 0)
return NULL;
switch (mode) {
case FISH_CBC_MODE:
if (strspn(data, base64_chars) != strlen(data))
return NULL;
ciphertext = (char *) g_base64_decode(data, &ciphertext_len);
break;
case FISH_ECB_MODE:
ciphertext = fish_base64_decode(data, &ciphertext_len);
}
if (ciphertext == NULL || ciphertext_len == 0)
return NULL;
plaintext = fish_cipher(ciphertext, ciphertext_len, key, keylen, 0, mode, final_len);
g_free(ciphertext);
if (*final_len == 0)
return NULL;
return plaintext;
}
/**
* Similar to fish_decrypt, but pad with zeros any after the first zero in the decrypted data
*
* @param [in] key Bytes of key
* @param [in] keylen Size of key
* @param [in] data Fish or standard Base64 encoded string
* @param [in] mode Chiper mode
* @return Array of char with data decrypted
*/
char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode) {
char *decrypted = NULL;
char *plaintext_str = NULL;
size_t decrypted_len = 0;
decrypted = fish_decrypt(key, strlen(key), data, mode, &decrypted_len);
if (decrypted == NULL || decrypted_len == 0)
return NULL;
plaintext_str = g_strndup(decrypted, decrypted_len);
g_free(decrypted);
return plaintext_str;
}
/**
* Determine if a nick have a key
*
* @param [in] nick Nickname
* @return TRUE if have a key or FALSE if not
*/
gboolean fish_nick_has_key(const char *nick) {
gboolean has_key = FALSE;
char *key;
enum fish_mode mode;
key = keystore_get_key(nick, &mode);
if (key) {
has_key = TRUE;
g_free(key);
};
return has_key;
}
/**
* Encrypts a message (see fish_encrypt). The key is searched for in the key store
*
* @param [in] nick Nickname
* @param [in] data Plaintext to encrypt
* @param [out] omode Mode of encryption
* @param [in] command_len Length of command to use without the message part
* @return A list of encoded strings with the message encrypted or NULL if any error occurred
*/
GSList *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode, size_t command_len) {
char *key;
GSList *encrypted_list = NULL;
char *encrypted = NULL;
enum fish_mode mode;
int max_len, max_chunks_len, chunks_len;
/* Look for key */
key = keystore_get_key(nick, &mode);
if (!key) return NULL;
*omode = mode;
/* Calculate max length of each line */
max_len = MAX_COMMAND_LENGTH - command_len;
/* Add '*' */
if (mode == FISH_CBC_MODE) max_len--;
max_chunks_len = max_text_command_len(max_len, mode);
const char *data_chunk = data;
while(foreach_utf8_data_chunks(data_chunk, max_chunks_len, &chunks_len)) {
encrypted = fish_encrypt(key, strlen(key), data_chunk, chunks_len, mode);
if (mode == FISH_CBC_MODE) {
/* Add '*' for CBC */
encrypted_list = g_slist_append(encrypted_list, g_strdup_printf("*%s", encrypted));
g_free(encrypted);
} else {
encrypted_list = g_slist_append(encrypted_list, encrypted);
}
/* Next chunk */
data_chunk += chunks_len;
}
return encrypted_list;
}
/**
* Decrypts a message (see fish_decrypt). The key is searched for in the key store
*
* @param [in] nick Nickname
* @param [in] data Plaintext to encrypt
* @param [out] omode Mode of encryption
* @return Plaintext message or NULL if any error occurred
*/
char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode) {
char *key;
char *decrypted;
enum fish_mode mode;
/* Look for key */
key = keystore_get_key(nick);
key = keystore_get_key(nick, &mode);
if (!key) return NULL;
*omode = mode;
if (mode == FISH_CBC_MODE)
++data;
/* Decrypt */
decrypted = fish_decrypt(key, strlen(key), data);
decrypted = fish_decrypt_str(key, strlen(key), data, mode);
g_free(key);
return decrypted;
}

View File

@ -1,6 +1,7 @@
/*
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -29,10 +30,21 @@
#include <glib.h>
char *fish_encrypt(const char *key, size_t keylen, const char *message);
char *fish_decrypt(const char *key, size_t keylen, const char *data);
char *fish_encrypt_for_nick(const char *nick, const char *data);
char *fish_decrypt_from_nick(const char *nick, const char *data);
enum fish_mode {
FISH_ECB_MODE = 0x1,
FISH_CBC_MODE = 0x2
};
int fish_init(void);
void fish_deinit(void);
char *fish_base64_encode(const char *message, size_t message_len);
char *fish_base64_decode(const char *message, size_t *final_len);
char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode);
char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len);
char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode);
gboolean fish_nick_has_key(const char *nick);
GSList *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode, size_t command_len);
char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode);
#endif

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Configuration">
<PlatformToolset>v140</PlatformToolset>
<PlatformToolset>v142</PlatformToolset>
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
@ -29,7 +29,7 @@
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;HAVE_DH_SET0_PQG;HAVE_DH_GET0_KEY;HAVE_DH_SET0_KEY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(Glib);..\..\src\common;$(HexChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
@ -40,7 +40,7 @@
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;HAVE_DH_SET0_PQG;HAVE_DH_GET0_KEY;HAVE_DH_SET0_KEY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(Glib);..\..\src\common;$(HexChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
@ -55,6 +55,7 @@
<ItemGroup>
<ClInclude Include="dh1080.h" />
<ClInclude Include="fish.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="irc.h" />
<ClInclude Include="keystore.h" />
<ClInclude Include="plugin_hexchat.h" />
@ -62,6 +63,7 @@
<ItemGroup>
<ClCompile Include="dh1080.c" />
<ClCompile Include="fish.c" />
<ClCompile Include="utils.c" />
<ClCompile Include="irc.c" />
<ClCompile Include="keystore.c" />
<ClCompile Include="plugin_hexchat.c" />

View File

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

View File

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

View File

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

View File

@ -2,9 +2,13 @@ if not libssl_dep.found()
error('fish plugin requires openssl')
endif
# Run tests
subdir('tests')
fishlim_sources = [
'dh1080.c',
'fish.c',
'utils.c',
'irc.c',
'keystore.c',
'plugin_hexchat.c'
@ -15,4 +19,5 @@ shared_module('fishlim', fishlim_sources,
install: true,
install_dir: plugindir,
name_prefix: '',
vs_module_defs: 'fishlim.def',
)

View File

@ -2,6 +2,7 @@
Copyright (c) 2010-2011 Samuel Lidén Borell <samuel@kodafritt.se>
Copyright (c) 2015 <the.cypher@gmail.com>
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -28,21 +29,21 @@
#include <glib.h>
#include <stdlib.h>
#include <string.h>
#include "hexchat-plugin.h"
#define HEXCHAT_MAX_WORDS 32
#include "fish.h"
#include "dh1080.h"
#include "keystore.h"
#include "irc.h"
static const char *fish_modes[] = {"", "ECB", "CBC", NULL};
static const char plugin_name[] = "FiSHLiM";
static const char plugin_desc[] = "Encryption plugin for the FiSH protocol. Less is More!";
static const char plugin_version[] = "0.1.0";
static const char plugin_version[] = "1.0.0";
static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] <password>, sets the key for a channel or nick";
static const char usage_delkey[] = "Usage: DELKEY <nick or #channel>, deletes the key for a channel or nick";
static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] [<mode>:]<password>, sets the key for a channel or nick. Modes: ECB, CBC";
static const char usage_delkey[] = "Usage: DELKEY [<nick or #channel>], deletes the key for a channel or nick";
static const char usage_keyx[] = "Usage: KEYX [<nick>], performs DH1080 key-exchange with <nick>";
static const char usage_topic[] = "Usage: TOPIC+ <topic>, sets a new encrypted topic for the current channel";
static const char usage_notice[] = "Usage: NOTICE+ <nick or #channel> <notice>";
@ -53,6 +54,13 @@ static hexchat_plugin *ph;
static GHashTable *pending_exchanges;
/**
* Compare two nicks using the current plugin
*/
int irc_nick_cmp(const char *a, const char *b) {
return hexchat_nickcmp (ph, a, b);
}
/**
* Returns the path to the key store file.
*/
@ -88,7 +96,7 @@ static hexchat_context *find_context_on_network (const char *name) {
int chan_id = hexchat_list_int(ph, channels, "id");
const char *chan_name = hexchat_list_str(ph, channels, "channel");
if (chan_id == id && chan_name && hexchat_nickcmp (ph, chan_name, name) == 0) {
if (chan_id == id && chan_name && irc_nick_cmp (chan_name, name) == 0) {
ret = (hexchat_context*)hexchat_list_str(ph, channels, "context");
break;
}
@ -98,8 +106,175 @@ static hexchat_context *find_context_on_network (const char *name) {
return ret;
}
int irc_nick_cmp(const char *a, const char *b) {
return hexchat_nickcmp (ph, a, b);
/**
* Retrive the field for own user in current context
* @return the field value
*/
char *get_my_info(const char *field, gboolean find_in_other_context) {
char *result = NULL;
const char *own_nick;
hexchat_list *list;
hexchat_context *ctx_current, *ctx_channel;
/* Display message */
own_nick = hexchat_get_info(ph, "nick");
if (!own_nick)
return NULL;
/* Get field for own nick if any */
list = hexchat_list_get(ph, "users");
if (list) {
while (hexchat_list_next(ph, list)) {
if (irc_nick_cmp(own_nick, hexchat_list_str(ph, list, "nick")) == 0)
result = g_strdup(hexchat_list_str(ph, list, field));
}
hexchat_list_free(ph, list);
}
if (result) {
return result;
}
/* Try to get from a channel (we are outside a channel) */
if (!find_in_other_context) {
return NULL;
}
list = hexchat_list_get(ph, "channels");
if (list) {
ctx_current = hexchat_get_context(ph);
while (hexchat_list_next(ph, list)) {
ctx_channel = (hexchat_context *) hexchat_list_str(ph, list, "context");
hexchat_set_context(ph, ctx_channel);
result = get_my_info(field, FALSE);
hexchat_set_context(ph, ctx_current);
if (result) {
break;
}
}
hexchat_list_free(ph, list);
}
return result;
}
/**
* Retrive the prefix character for own nick in current context
* @return @ or + or NULL
*/
char *get_my_own_prefix(void) {
return get_my_info("prefix", FALSE);
}
/**
* Retrive the mask for own nick in current context
* @return Host name in the form: user@host (or NULL if not known)
*/
char *get_my_own_host(void) {
return get_my_info("host", TRUE);
}
/**
* Calculate the length of prefix for current user in current context
*
* @return Length of prefix
*/
int get_prefix_length(void) {
char *own_host;
int prefix_len = 0;
/* ':! ' + 'nick' + 'ident@host', e.g. ':user!~name@mynet.com ' */
prefix_len = 3 + strlen(hexchat_get_info(ph, "nick"));
own_host = get_my_own_host();
if (own_host) {
prefix_len += strlen(own_host);
} else {
/* https://stackoverflow.com/questions/8724954/what-is-the-maximum-number-of-characters-for-a-host-name-in-unix */
prefix_len += 64;
}
g_free(own_host);
return prefix_len;
}
/**
* Try to decrypt the first occurrence of fish message
*
* @param message Message to decrypt
* @param key Key of message
* @return Array of char with decrypted message or NULL. The returned string
* should be freed with g_free() when no longer needed.
*/
char *decrypt_raw_message(const char *message, const char *key) {
const char *prefixes[] = {"+OK ", "mcps ", NULL};
char *start = NULL, *end = NULL;
char *left = NULL, *right = NULL;
char *encrypted = NULL, *decrypted = NULL;
int length = 0;
int index_prefix;
enum fish_mode mode;
GString *message_decrypted;
char *result = NULL;
if (message == NULL || key == NULL)
return NULL;
for (index_prefix = 0; index_prefix < 2; index_prefix++) {
start = g_strstr_len(message, strlen(message), prefixes[index_prefix]);
if (start) {
/* Length ALWAYS will be less that original message
* add '[CBC] ' length */
message_decrypted = g_string_sized_new(strlen(message) + 6);
/* Left part of message */
left = g_strndup(message, start - message);
g_string_append(message_decrypted, left);
g_free(left);
/* Encrypted part */
start += strlen(prefixes[index_prefix]);
end = g_strstr_len(start, strlen(message), " ");
if (end) {
length = end - start;
right = end;
}
if (length > 0) {
encrypted = g_strndup(start, length);
} else {
encrypted = g_strdup(start);
}
decrypted = fish_decrypt_from_nick(key, encrypted, &mode);
g_free(encrypted);
if (decrypted == NULL) {
g_string_free(message_decrypted, TRUE);
return NULL;
}
/* Add encrypted flag */
g_string_append(message_decrypted, "[");
g_string_append(message_decrypted, fish_modes[mode]);
g_string_append(message_decrypted, "] ");
/* Decrypted message */
g_string_append(message_decrypted, decrypted);
g_free(decrypted);
/* Right part of message */
if (right) {
g_string_append(message_decrypted, right);
}
result = message_decrypted->str;
g_string_free(message_decrypted, FALSE);
return result;
}
}
return NULL;
}
/*static int handle_debug(char *word[], char *word_eol[], void *userdata) {
@ -115,20 +290,49 @@ int irc_nick_cmp(const char *a, const char *b) {
* Called when a message is to be sent.
*/
static int handle_outgoing(char *word[], char *word_eol[], void *userdata) {
const char *own_nick;
/* Encrypt the message if possible */
char *prefix;
enum fish_mode mode;
char *message;
GString *command;
GSList *encrypted_list, *encrypted_item;
const char *channel = hexchat_get_info(ph, "channel");
char *encrypted = fish_encrypt_for_nick(channel, word_eol[1]);
if (!encrypted) return HEXCHAT_EAT_NONE;
/* Check if we can encrypt */
if (!fish_nick_has_key(channel)) return HEXCHAT_EAT_NONE;
command = g_string_new("");
g_string_printf(command, "PRIVMSG %s :+OK ", channel);
encrypted_list = fish_encrypt_for_nick(channel, word_eol[1], &mode, get_prefix_length() + command->len);
if (!encrypted_list) {
g_string_free(command, TRUE);
return HEXCHAT_EAT_NONE;
}
/* Get prefix for own nick if any */
prefix = get_my_own_prefix();
/* Add encrypted flag */
message = g_strconcat("[", fish_modes[mode], "] ", word_eol[1], NULL);
/* Display message */
own_nick = hexchat_get_info(ph, "nick");
hexchat_emit_print(ph, "Your Message", own_nick, word_eol[1], NULL);
/* Send message */
hexchat_commandf(ph, "PRIVMSG %s :+OK %s", channel, encrypted);
g_free(encrypted);
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), message, prefix, NULL);
g_free(message);
/* Send encrypted messages */
encrypted_item = encrypted_list;
while (encrypted_item)
{
hexchat_commandf(ph, "%s%s", command->str, (char *)encrypted_item->data);
encrypted_item = encrypted_item->next;
}
g_free(prefix);
g_string_free(command, TRUE);
g_slist_free_full(encrypted_list, g_free);
return HEXCHAT_EAT_HEXCHAT;
}
@ -139,96 +343,59 @@ static int handle_incoming(char *word[], char *word_eol[], hexchat_event_attrs *
const char *prefix;
const char *command;
const char *recipient;
const char *encrypted;
const char *peice;
const char *raw_message = word_eol[1];
char *sender_nick;
char *decrypted;
size_t w;
size_t ew;
size_t uw;
char prefix_char = 0;
size_t parameters_offset;
GString *message;
if (!irc_parse_message((const char **)word, &prefix, &command, &w))
if (!irc_parse_message((const char **)word, &prefix, &command, &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,19 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
sender = irc_prefix_get_nick(prefix);
query_ctx = find_context_on_network(sender);
if (query_ctx)
hexchat_set_context(ph, query_ctx);
g_assert(hexchat_set_context(ph, query_ctx) == 1);
dh_message++; /* : prefix */
if (*dh_message == '+' || *dh_message == '-')
dh_message++; /* identify-msg */
cbc = g_strcmp0 (word[6], "CBC") == 0;
if (g_strcmp0 (word[6], "CBC") == 0)
mode = FISH_CBC_MODE;
if (!strcmp(dh_message, "DH1080_INIT")) {
char *pub_key;
if (cbc) {
hexchat_print(ph, "Received key exchange for CBC mode which is not supported.");
goto cleanup;
}
hexchat_printf(ph, "Received public key from %s, sending mine...", sender);
hexchat_printf(ph, "Received public key from %s (%s), sending mine...", sender, fish_modes[mode]);
if (dh1080_generate_key(&priv_key, &pub_key)) {
hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s", sender, pub_key);
hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s%s", sender, pub_key, (mode == FISH_CBC_MODE) ? " CBC" : "");
g_free(pub_key);
} else {
hexchat_print(ph, "Failed to generate keys");
@ -279,11 +440,6 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
g_hash_table_steal(pending_exchanges, sender_lower);
g_free(sender_lower);
if (cbc) {
hexchat_print(ph, "Received key exchange for CBC mode which is not supported.");
goto cleanup;
}
if (!priv_key) {
hexchat_printf(ph, "Received a key exchange response for unknown user: %s", sender);
goto cleanup;
@ -295,8 +451,8 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
}
if (dh1080_compute_key(priv_key, dh_pubkey, &secret_key)) {
keystore_store_key(sender, secret_key);
hexchat_printf(ph, "Stored new key for %s", sender);
keystore_store_key(sender, secret_key, mode);
hexchat_printf(ph, "Stored new key for %s (%s)", sender, fish_modes[mode]);
g_free(secret_key);
} else {
hexchat_print(ph, "Failed to create secret key!");
@ -314,6 +470,7 @@ cleanup:
static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
const char *nick;
const char *key;
enum fish_mode mode;
/* Check syntax */
if (*word[2] == '\0') {
@ -331,9 +488,17 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
key = word_eol[3];
}
mode = FISH_ECB_MODE;
if (g_ascii_strncasecmp("cbc:", key, 4) == 0) {
key = key+4;
mode = FISH_CBC_MODE;
} else if (g_ascii_strncasecmp("ecb:", key, 4) == 0) {
key = key+4;
}
/* Set password */
if (keystore_store_key(nick, key)) {
hexchat_printf(ph, "Stored key for %s\n", nick);
if (keystore_store_key(nick, key, mode)) {
hexchat_printf(ph, "Stored key for %s (%s)\n", nick, fish_modes[mode]);
} else {
hexchat_printf(ph, "\00305Failed to store key in addon_fishlim.conf\n");
}
@ -345,22 +510,30 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
* Command handler for /delkey
*/
static int handle_delkey(char *word[], char *word_eol[], void *userdata) {
const char *nick;
char *nick = NULL;
int ctx_type = 0;
/* Check syntax */
if (*word[2] == '\0' || *word[3] != '\0') {
hexchat_printf(ph, "%s\n", usage_delkey);
return HEXCHAT_EAT_HEXCHAT;
/* Delete key from input */
if (*word[2] != '\0') {
nick = g_strstrip(g_strdup(word_eol[2]));
} else { /* Delete key from current context */
nick = g_strdup(hexchat_get_info(ph, "channel"));
ctx_type = hexchat_list_int(ph, NULL, "type");
/* Only allow channel or dialog */
if (ctx_type < 2 || ctx_type > 3) {
hexchat_printf(ph, "%s\n", usage_delkey);
return HEXCHAT_EAT_HEXCHAT;
}
}
nick = g_strstrip (word_eol[2]);
/* Delete the given nick from the key store */
if (keystore_delete_nick(nick)) {
hexchat_printf(ph, "Deleted key for %s\n", nick);
} else {
hexchat_printf(ph, "\00305Failed to delete key in addon_fishlim.conf!\n");
}
g_free(nick);
return HEXCHAT_EAT_HEXCHAT;
}
@ -379,7 +552,7 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
}
if (query_ctx) {
hexchat_set_context(ph, query_ctx);
g_assert(hexchat_set_context(ph, query_ctx) == 1);
ctx_type = hexchat_list_int(ph, NULL, "type");
}
@ -391,8 +564,8 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
if (dh1080_generate_key(&priv_key, &pub_key)) {
g_hash_table_replace (pending_exchanges, g_ascii_strdown(target, -1), priv_key);
hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s", target, pub_key);
hexchat_printf(ph, "Sent public key to %s, waiting for reply...", target);
hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s CBC", target, pub_key);
hexchat_printf(ph, "Sent public key to %s (CBC), waiting for reply...", target);
g_free(pub_key);
} else {
@ -408,7 +581,9 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) {
const char *target;
const char *topic = word_eol[2];
char *buf;
enum fish_mode mode;
GString *command;
GSList *encrypted_list;
if (!*topic) {
hexchat_print(ph, usage_topic);
@ -421,40 +596,77 @@ static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) {
}
target = hexchat_get_info(ph, "channel");
buf = fish_encrypt_for_nick(target, topic);
if (buf == NULL) {
/* Check if we can encrypt */
if (!fish_nick_has_key(target)) {
hexchat_printf(ph, "/topic+ error, no key found for %s", target);
return HEXCHAT_EAT_ALL;
}
hexchat_commandf(ph, "TOPIC %s +OK %s", target, buf);
g_free(buf);
return HEXCHAT_EAT_ALL;
command = g_string_new("");
g_string_printf(command, "TOPIC %s +OK ", target);
encrypted_list = fish_encrypt_for_nick(target, topic, &mode, get_prefix_length() + command->len);
if (!encrypted_list) {
g_string_free(command, TRUE);
hexchat_printf(ph, "/topic+ error, can't encrypt %s", target);
return HEXCHAT_EAT_ALL;
}
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_list->data);
g_string_free(command, TRUE);
g_slist_free_full(encrypted_list, g_free);
return HEXCHAT_EAT_ALL;
}
/**
* Command handler for /notice+
*/
static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata)
{
static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata) {
const char *target = word[2];
const char *notice = word_eol[3];
char *buf;
char *notice_flag;
enum fish_mode mode;
GString *command;
GSList *encrypted_list, *encrypted_item;
if (!*target || !*notice) {
hexchat_print(ph, usage_notice);
return HEXCHAT_EAT_ALL;
}
buf = fish_encrypt_for_nick(target, notice);
if (buf == NULL) {
/* Check if we can encrypt */
if (!fish_nick_has_key(target)) {
hexchat_printf(ph, "/notice+ error, no key found for %s.", target);
return HEXCHAT_EAT_ALL;
}
hexchat_commandf(ph, "quote NOTICE %s :+OK %s", target, buf);
hexchat_emit_print(ph, "Notice Sent", target, notice);
g_free(buf);
command = g_string_new("");
g_string_printf(command, "quote NOTICE %s :+OK ", target);
encrypted_list = fish_encrypt_for_nick(target, notice, &mode, get_prefix_length() + command->len);
if (!encrypted_list) {
g_string_free(command, TRUE);
hexchat_printf(ph, "/notice+ error, can't encrypt %s", target);
return HEXCHAT_EAT_ALL;
}
notice_flag = g_strconcat("[", fish_modes[mode], "] ", notice, NULL);
hexchat_emit_print(ph, "Notice Send", target, notice_flag);
/* Send encrypted messages */
encrypted_item = encrypted_list;
while (encrypted_item) {
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_item->data);
encrypted_item = encrypted_item->next;
}
g_free(notice_flag);
g_string_free(command, TRUE);
g_slist_free_full(encrypted_list, g_free);
return HEXCHAT_EAT_ALL;
}
@ -462,56 +674,102 @@ static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata)
/**
* Command handler for /msg+
*/
static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata)
{
static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) {
const char *target = word[2];
const char *message = word_eol[3];
char *message_flag;
char *prefix;
hexchat_context *query_ctx;
char *buf;
enum fish_mode mode;
GString *command;
GSList *encrypted_list, *encrypted_item;
if (!*target || !*message) {
hexchat_print(ph, usage_msg);
return HEXCHAT_EAT_ALL;
}
buf = fish_encrypt_for_nick(target, message);
if (buf == NULL) {
/* Check if we can encrypt */
if (!fish_nick_has_key(target)) {
hexchat_printf(ph, "/msg+ error, no key found for %s", target);
return HEXCHAT_EAT_ALL;
}
hexchat_commandf(ph, "PRIVMSG %s :+OK %s", target, buf);
command = g_string_new("");
g_string_printf(command, "PRIVMSG %s :+OK ", target);
encrypted_list = fish_encrypt_for_nick(target, message, &mode, get_prefix_length() + command->len);
if (!encrypted_list) {
g_string_free(command, TRUE);
hexchat_printf(ph, "/msg+ error, can't encrypt %s", target);
return HEXCHAT_EAT_ALL;
}
/* Send encrypted messages */
encrypted_item = encrypted_list;
while (encrypted_item) {
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_item->data);
encrypted_item = encrypted_item->next;
}
g_string_free(command, TRUE);
g_slist_free_full(encrypted_list, g_free);
query_ctx = find_context_on_network(target);
if (query_ctx) {
hexchat_set_context(ph, query_ctx);
g_assert(hexchat_set_context(ph, query_ctx) == 1);
/* FIXME: Mode char */
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"),
message, "", "");
}
else {
prefix = get_my_own_prefix();
/* Add encrypted flag */
message_flag = g_strconcat("[", fish_modes[mode], "] ", message, NULL);
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), message_flag, prefix, NULL);
g_free(prefix);
g_free(message_flag);
} else {
hexchat_emit_print(ph, "Message Send", target, message);
}
g_free(buf);
return HEXCHAT_EAT_ALL;
}
static int handle_crypt_me(char *word[], char *word_eol[], void *userdata) {
const char *channel = hexchat_get_info(ph, "channel");
char *buf;
const char *channel = hexchat_get_info(ph, "channel");
enum fish_mode mode;
GString *command;
GSList *encrypted_list, *encrypted_item;
buf = fish_encrypt_for_nick(channel, word_eol[2]);
if (!buf)
/* Check if we can encrypt */
if (!fish_nick_has_key(channel)) {
return HEXCHAT_EAT_NONE;
}
hexchat_commandf(ph, "PRIVMSG %s :\001ACTION +OK %s \001", channel, buf);
hexchat_emit_print(ph, "Your Action", hexchat_get_info(ph, "nick"),
word_eol[2], NULL);
command = g_string_new("");
g_string_printf(command, "PRIVMSG %s :\001ACTION +OK ", channel);
g_free(buf);
return HEXCHAT_EAT_ALL;
/* 2 = ' \001' */
encrypted_list = fish_encrypt_for_nick(channel, word_eol[2], &mode, get_prefix_length() + command->len + 2);
if (!encrypted_list) {
g_string_free(command, TRUE);
hexchat_printf(ph, "/me error, can't encrypt %s", channel);
return HEXCHAT_EAT_ALL;
}
hexchat_emit_print(ph, "Your Action", hexchat_get_info(ph, "nick"), word_eol[2], NULL);
/* Send encrypted messages */
encrypted_item = encrypted_list;
while (encrypted_item) {
hexchat_commandf(ph, "%s%s \001", command->str, (char *) encrypted_item->data);
encrypted_item = encrypted_item->next;
}
g_string_free(command, TRUE);
g_slist_free_full(encrypted_list, g_free);
return HEXCHAT_EAT_ALL;
}
/**
@ -557,6 +815,9 @@ int hexchat_plugin_init(hexchat_plugin *plugin_handle,
hexchat_hook_server_attrs(ph, "TOPIC", HEXCHAT_PRI_NORM, handle_incoming, NULL);
hexchat_hook_server_attrs(ph, "332", HEXCHAT_PRI_NORM, handle_incoming, NULL);
if (!fish_init())
return 0;
if (!dh1080_init())
return 0;
@ -570,6 +831,7 @@ int hexchat_plugin_init(hexchat_plugin *plugin_handle,
int hexchat_plugin_deinit(void) {
g_clear_pointer(&pending_exchanges, g_hash_table_destroy);
dh1080_deinit();
fish_deinit();
hexchat_printf(ph, "%s plugin unloaded\n", plugin_name);
return 1;

View File

@ -0,0 +1,16 @@
fishlim_test_sources = [
'tests.c',
'mock-keystore.c',
'../fish.c',
'../utils.c',
]
fishlim_tests = executable('fishlim_tests', fishlim_test_sources,
dependencies: [libgio_dep, libssl_dep, hexchat_plugin_dep],
include_directories: include_directories('..'),
)
test('Fishlim Tests', fishlim_tests,
protocol: 'tap',
timeout: 600,
)

View File

@ -0,0 +1,51 @@
/*
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include "fish.h"
/**
* Extracts a key from the key store file.
*/
char *
keystore_get_key(const char *nick, enum fish_mode *mode)
{
return NULL;
}
/**
* Sets a key in the key store file.
*/
gboolean
keystore_store_key(const char *nick, const char *key, enum fish_mode mode)
{
return TRUE;
}
/**
* Deletes a nick from the key store.
*/
gboolean
keystore_delete_nick(const char *nick)
{
return TRUE;
}

View File

@ -0,0 +1,285 @@
/*
Copyright (c) 2020 <bakasura@protonmail.ch>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include <string.h>
#include <glib.h>
#include "fish.h"
#include "utils.h"
/**
* Auxiliary function: Generate a random string
* @param out Preallocated string to fill
* @param len Size of bytes to fill
*/
static void
random_string(char *out, size_t len)
{
GRand *rand = NULL;
int i = 0;
rand = g_rand_new();
for (i = 0; i < len; ++i) {
out[i] = g_rand_int_range(rand, 1, 256);
}
out[len] = 0;
g_rand_free(rand);
}
/**
* Check encrypt and decrypt in ECB mode
*/
static void
test_ecb(void)
{
char *b64 = NULL;
char *de = NULL;
int key_len, message_len = 0;
char key[57];
char message[1000];
/* Generate key 32448 bits (Yes, I start with 8 bits) */
for (key_len = 1; key_len < 57; ++key_len) {
random_string(key, key_len);
for (message_len = 1; message_len < 1000; ++message_len) {
random_string(message, message_len);
/* Encrypt */
b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE);
g_assert_nonnull(b64);
/* Decrypt */
/* Linear */
de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE);
g_assert_cmpstr (de, ==, message);
g_free(de);
/* Mixed */
de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE);
g_assert_cmpstr (de, ==, message);
g_free(de);
g_free(b64);
}
}
}
/**
* Check encrypt and decrypt in CBC mode
*/
static void
test_cbc(void)
{
char *b64 = NULL;
char *de = NULL;
int key_len, message_len = 0;
char key[57];
char message[1000];
/* Generate key 32448 bits (Yes, I start with 8 bits) */
for (key_len = 1; key_len < 57; ++key_len) {
random_string(key, key_len);
for (message_len = 1; message_len < 1000; ++message_len) {
random_string(message, message_len);
/* Encrypt */
b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE);
g_assert_nonnull(b64);
/* Decrypt */
/* Linear */
de = fish_decrypt_str(key, key_len, b64, FISH_CBC_MODE);
g_assert_cmpstr (de, ==, message);
g_free(de);
g_free(b64);
}
}
}
/**
* Check the calculation of final length from an encoded string in Base64
*/
static void
test_base64_len (void)
{
char *b64 = NULL;
char message[1000];
int message_end = sizeof (message) - 1;
random_string(message, message_end);
for (; message_end >= 0; --message_end) {
message[message_end] = '\0'; /* Truncate instead of generating new strings */
b64 = g_base64_encode((const unsigned char *) message, message_end);
g_assert_nonnull(b64);
g_assert_cmpuint(strlen(b64), == , base64_len(message_end));
g_free(b64);
}
}
/**
* Check the calculation of final length from an encoded string in BlowcryptBase64
*/
static void
test_base64_fish_len (void)
{
char *b64 = NULL;
int message_len = 0;
char message[1000];
for (message_len = 1; message_len < 1000; ++message_len) {
random_string(message, message_len);
b64 = fish_base64_encode(message, message_len);
g_assert_nonnull(b64);
g_assert_cmpuint(strlen(b64), == , base64_fish_len(message_len));
g_free(b64);
}
}
/**
* Check the calculation of final length from an encrypted string in ECB mode
*/
static void
test_base64_ecb_len(void)
{
char *b64 = NULL;
int key_len, message_len = 0;
char key[57];
char message[1000];
/* Generate key 32448 bits (Yes, I start with 8 bits) */
for (key_len = 1; key_len < 57; ++key_len) {
random_string(key, key_len);
for (message_len = 1; message_len < 1000; ++message_len) {
random_string(message, message_len);
b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE);
g_assert_nonnull(b64);
g_assert_cmpuint(strlen(b64), == , ecb_len(message_len));
g_free(b64);
}
}
}
/**
* Check the calculation of final length from an encrypted string in CBC mode
*/
static void
test_base64_cbc_len(void)
{
char *b64 = NULL;
int key_len, message_len = 0;
char key[57];
char message[1000];
/* Generate key 32448 bits (Yes, I start with 8 bits) */
for (key_len = 1; key_len < 57; ++key_len) {
random_string(key, key_len);
for (message_len = 1; message_len < 1000; ++message_len) {
random_string(message, message_len);
b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE);
g_assert_nonnull(b64);
g_assert_cmpuint(strlen(b64), == , cbc_len(message_len));
g_free(b64);
}
}
}
/**
* Check the calculation of length limit for a plaintext in each encryption mode
*/
static void
test_max_text_command_len(void)
{
int max_encoded_len, plaintext_len;
enum fish_mode mode;
for (max_encoded_len = 0; max_encoded_len < 10000; ++max_encoded_len) {
for (mode = FISH_ECB_MODE; mode <= FISH_CBC_MODE; ++mode) {
plaintext_len = max_text_command_len(max_encoded_len, mode);
g_assert_cmpuint(encoded_len(plaintext_len, mode), <= , max_encoded_len);
}
}
}
/**
* Check the calculation of length limit for a plaintext in each encryption mode
*/
static void
test_foreach_utf8_data_chunks(void)
{
GRand *rand = NULL;
GString *chunks = NULL;
int max_chunks_len, chunks_len;
char ascii_message[1001];
char *data_chunk = NULL;
rand = g_rand_new();
max_chunks_len = g_rand_int_range(rand, 2, 301);
random_string(ascii_message, 1000);
data_chunk = ascii_message;
chunks = g_string_new(NULL);
while (foreach_utf8_data_chunks(data_chunk, max_chunks_len, &chunks_len)) {
g_string_append(chunks, g_strndup(data_chunk, chunks_len));
/* Next chunk */
data_chunk += chunks_len;
}
/* Check data loss */
g_assert_cmpstr(chunks->str, == , ascii_message);
g_string_free(chunks, TRUE);
g_rand_free (rand);
}
int
main(int argc, char *argv[]) {
g_test_init(&argc, &argv, NULL);
g_test_add_func("/fishlim/ecb", test_ecb);
g_test_add_func("/fishlim/cbc", test_cbc);
g_test_add_func("/fishlim/base64_len", test_base64_len);
g_test_add_func("/fishlim/base64_fish_len", test_base64_fish_len);
g_test_add_func("/fishlim/base64_ecb_len", test_base64_ecb_len);
g_test_add_func("/fishlim/base64_cbc_len", test_base64_cbc_len);
g_test_add_func("/fishlim/max_text_command_len", test_max_text_command_len);
g_test_add_func("/fishlim/foreach_utf8_data_chunks", test_foreach_utf8_data_chunks);
fish_init();
int ret = g_test_run();
fish_deinit();
return ret;
}

151
plugins/fishlim/utils.c Normal file
View File

@ -0,0 +1,151 @@
/*
Copyright (c) 2020 <bakasura@protonmail.ch>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include <string.h>
#include "utils.h"
#include "fish.h"
/**
* Calculate the length of Base64-encoded string
*
* @param plaintext_len Size of clear text to encode
* @return Size of encoded string
*/
unsigned long base64_len(size_t plaintext_len) {
int length_unpadded = (4 * plaintext_len) / 3;
/* Add padding */
return length_unpadded % 4 != 0 ? length_unpadded + (4 - length_unpadded % 4) : length_unpadded;
}
/**
* Calculate the length of BlowcryptBase64-encoded string
*
* @param plaintext_len Size of clear text to encode
* @return Size of encoded string
*/
unsigned long base64_fish_len(size_t plaintext_len) {
int length_unpadded = (12 * plaintext_len) / 8;
/* Add padding */
return length_unpadded % 12 != 0 ? length_unpadded + (12 - length_unpadded % 12) : length_unpadded;
}
/**
* Calculate the length of fish-encrypted string in CBC mode
*
* @param plaintext_len Size of clear text to encode
* @return Size of encoded string
*/
unsigned long cbc_len(size_t plaintext_len) {
/*IV + DATA + Zero Padding */
return base64_len(8 + (plaintext_len % 8 != 0 ? plaintext_len + 8 - (plaintext_len % 8) : plaintext_len));
}
/**
* Calculate the length of fish-encrypted string in ECB mode
*
* @param plaintext_len Size of clear text to encode
* @return Size of encoded string
*/
unsigned long ecb_len(size_t plaintext_len) {
return base64_fish_len(plaintext_len);
}
/**
* Calculate the length of encrypted string in 'mode' mode
*
* @param plaintext_len Length of plaintext
* @param mode Encryption mode
* @return Size of encoded string
*/
unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode) {
switch (mode) {
case FISH_CBC_MODE:
return cbc_len(plaintext_len);
break;
case FISH_ECB_MODE:
return ecb_len(plaintext_len);
}
return 0;
}
/**
* Determine the maximum length of plaintext for a 'max_len' limit taking care the overload of encryption
*
* @param max_len Limit for plaintext
* @param mode Encryption mode
* @return Maximum allowed plaintext length
*/
int max_text_command_len(size_t max_len, enum fish_mode mode) {
int len;
for (len = max_len; encoded_len(len, mode) > max_len; --len);
return len;
}
/**
* Iterate over 'data' in chunks of 'max_chunk_len' taking care the UTF-8 characters
*
* @param data Data to iterate
* @param max_chunk_len Size of biggest chunk
* @param [out] chunk_len Current chunk length
* @return Pointer to current chunk position or NULL if not have more chunks
*/
const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len) {
int data_len, last_chunk_len = 0;
if (!*data) {
return NULL;
}
/* Last chunk of data */
data_len = strlen(data);
if (data_len <= max_chunk_len) {
*chunk_len = data_len;
return data;
}
*chunk_len = 0;
const char *utf8_character = data;
/* Not valid UTF-8, but maybe valid text, just split into max length */
if (!g_utf8_validate(data, -1, NULL)) {
*chunk_len = max_chunk_len;
return utf8_character;
}
while (*utf8_character && *chunk_len <= max_chunk_len) {
last_chunk_len = *chunk_len;
*chunk_len = (g_utf8_next_char(utf8_character) - data) * sizeof(*utf8_character);
utf8_character = g_utf8_next_char(utf8_character);
}
/* We need the previous length before overflow the limit */
*chunk_len = last_chunk_len;
return utf8_character;
}

39
plugins/fishlim/utils.h Normal file
View File

@ -0,0 +1,39 @@
/*
Copyright (c) 2020 <bakasura@protonmail.ch>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef PLUGIN_HEXCHAT_FISHLIM_UTILS_H
#define PLUGIN_HEXCHAT_FISHLIM_UTILS_H
#include <stddef.h>
#include "fish.h"
unsigned long base64_len(size_t plaintext_len);
unsigned long base64_fish_len(size_t plaintext_len);
unsigned long cbc_len(size_t plaintext_len);
unsigned long ecb_len(size_t plaintext_len);
unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode);
int max_text_command_len(size_t max_len, enum fish_mode mode);
const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len);
#endif

View File

@ -35,6 +35,8 @@
#include <hexchat-plugin.h>
#define WORD_ARRAY_LEN 32
static char plugin_name[] = "Lua";
static char plugin_description[] = "Lua scripting interface";
static char plugin_version[16] = "1.3";
@ -275,13 +277,13 @@ static int api_command_closure(char *word[], char *word_eol[], void *udata)
base = lua_gettop(L);
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
lua_newtable(L);
for(i = 1; i < 32 && *word_eol[i]; i++)
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
{
lua_pushstring(L, word[i]);
lua_rawseti(L, -2, i);
}
lua_newtable(L);
for(i = 1; i < 32 && *word_eol[i]; i++)
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
{
lua_pushstring(L, word_eol[i]);
lua_rawseti(L, -2, i);
@ -462,13 +464,13 @@ static int api_server_closure(char *word[], char *word_eol[], void *udata)
base = lua_gettop(L);
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
lua_newtable(L);
for(i = 1; i < 32 && *word_eol[i]; i++)
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
{
lua_pushstring(L, word[i]);
lua_rawseti(L, -2, i);
}
lua_newtable(L);
for(i = 1; i < 32 && *word_eol[i]; i++)
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
{
lua_pushstring(L, word_eol[i]);
lua_rawseti(L, -2, i);
@ -521,13 +523,13 @@ static int api_server_attrs_closure(char *word[], char *word_eol[], hexchat_even
base = lua_gettop(L);
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
lua_newtable(L);
for(i = 1; i < 32 && *word_eol[i]; i++)
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
{
lua_pushstring(L, word[i]);
lua_rawseti(L, -2, i);
}
lua_newtable(L);
for(i = 1; i < 32 && *word_eol[i]; i++)
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
{
lua_pushstring(L, word_eol[i]);
lua_rawseti(L, -2, i);
@ -955,16 +957,21 @@ static int api_hexchat_pluginprefs_meta_pairs(lua_State *L)
hexchat_plugin *h;
if(!script->name)
return luaL_error(L, "cannot use hexchat.pluginprefs before registering with hexchat.register");
dest = lua_newuserdata(L, 4096);
h = script->handle;
if(!hexchat_pluginpref_list(h, dest))
strcpy(dest, "");
lua_pushlightuserdata(L, dest);
lua_pushlightuserdata(L, dest);
lua_pushcclosure(L, api_hexchat_pluginprefs_meta_pairs_closure, 2);
return 1;
lua_insert(L, -2); // Return the userdata (second return value from pairs),
// even though it's not used by the closure (first return
// value from pairs), so that Lua knows not to GC it.
return 2;
}
static int api_attrs_meta_index(lua_State *L)
@ -1187,11 +1194,11 @@ static void patch_clibs(lua_State *L)
if(lua_type(L, -2) == LUA_TLIGHTUSERDATA && lua_type(L, -1) == LUA_TTABLE)
{
lua_setfield(L, LUA_REGISTRYINDEX, "_CLIBS");
lua_pop(L, 1);
break;
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
static GPtrArray *scripts;
@ -1762,4 +1769,3 @@ G_MODULE_EXPORT int hexchat_plugin_deinit(hexchat_plugin *plugin_handle)
g_clear_pointer(&expand_buffer, g_free);
return 1;
}

View File

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

View File

@ -26,7 +26,7 @@ if get_option('with-lua') != 'false'
subdir('lua')
endif
if get_option('with-perl')
if get_option('with-perl') != 'false'
subdir('perl')
endif

View File

@ -51,7 +51,7 @@ sub get_context;
sub HexChat::Internal::context_info;
sub HexChat::Internal::print;
#keep compability with Xchat scripts
#keep compatibility with Xchat scripts
sub EAT_XCHAT ();
BEGIN {
*Xchat:: = *HexChat::;

View File

@ -13,19 +13,24 @@ hexchat_perl_module = custom_target('hexchat-perl-header',
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
)
irc_perl_module = custom_target('irc-perl-header',
input: 'lib/IRC.pm',
output: 'irc.pm.h',
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
)
perl_cflags = []
irc_perl_module = []
perl = find_program('perl')
if get_option('with-perl-legacy-api')
irc_perl_module = custom_target('irc-perl-header',
input: 'lib/IRC.pm',
output: 'irc.pm.h',
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
)
perl_cflags += '-DOLD_PERL'
endif
perl = find_program(get_option('with-perl'))
ret = run_command([perl, '-MExtUtils::Embed', '-e', 'ccopts'])
if ret.returncode() != 0
error('perl: Failed to get cflags')
endif
perl_cflags = []
foreach flag : ret.stdout().strip().split(' ')
if flag.startswith('-I') or flag.startswith('-D')
perl_cflags += flag
@ -71,13 +76,17 @@ int main(void) {
error('found perl not suitable for plugin')
endif
perl_dep = declare_dependency(
compile_args: perl_cflags,
link_args: perl_ldflags
)
shared_module('perl',
sources: ['perl.c', hexchat_perl_module, irc_perl_module],
dependencies: [libgio_dep, hexchat_plugin_dep],
c_args: perl_cflags,
link_args: perl_ldflags,
dependencies: [libgio_dep, hexchat_plugin_dep, perl_dep],
install: true,
install_dir: plugindir,
install_rpath: perl_rpath,
name_prefix: '',
vs_module_defs: 'perl.def',
)

View File

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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Configuration">
<PlatformToolset>v140</PlatformToolset>
<PlatformToolset>v142</PlatformToolset>
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">

386
plugins/python/_hexchat.py Normal file
View File

@ -0,0 +1,386 @@
import inspect
import sys
from contextlib import contextmanager
from _hexchat_embedded import ffi, lib
__all__ = [
'EAT_ALL', 'EAT_HEXCHAT', 'EAT_NONE', 'EAT_PLUGIN', 'EAT_XCHAT',
'PRI_HIGH', 'PRI_HIGHEST', 'PRI_LOW', 'PRI_LOWEST', 'PRI_NORM',
'__doc__', '__version__', 'command', 'del_pluginpref', 'emit_print',
'find_context', 'get_context', 'get_info',
'get_list', 'get_lists', 'get_pluginpref', 'get_prefs', 'hook_command',
'hook_print', 'hook_print_attrs', 'hook_server', 'hook_server_attrs',
'hook_timer', 'hook_unload', 'list_pluginpref', 'nickcmp', 'prnt',
'set_pluginpref', 'strip', 'unhook',
]
__doc__ = 'HexChat Scripting Interface'
__version__ = (2, 0)
__license__ = 'GPL-2.0+'
EAT_NONE = 0
EAT_HEXCHAT = 1
EAT_XCHAT = EAT_HEXCHAT
EAT_PLUGIN = 2
EAT_ALL = EAT_HEXCHAT | EAT_PLUGIN
PRI_LOWEST = -128
PRI_LOW = -64
PRI_NORM = 0
PRI_HIGH = 64
PRI_HIGHEST = 127
# We need each module to be able to reference their parent plugin
# which is a bit tricky since they all share the exact same module.
# Simply navigating up to what module called it seems to actually
# be a fairly reliable and simple method of doing so if ugly.
def __get_current_plugin():
frame = inspect.stack()[1][0]
while '__plugin' not in frame.f_globals:
frame = frame.f_back
assert frame is not None
return frame.f_globals['__plugin']
# Keeping API compat
if sys.version_info[0] == 2:
def __decode(string):
return string
else:
def __decode(string):
return string.decode()
# ------------ API ------------
def prnt(string):
lib.hexchat_print(lib.ph, string.encode())
def emit_print(event_name, *args, **kwargs):
time = kwargs.pop('time', 0) # For py2 compat
cargs = []
for i in range(4):
arg = args[i].encode() if len(args) > i else b''
cstring = ffi.new('char[]', arg)
cargs.append(cstring)
if time == 0:
return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs)
attrs = lib.hexchat_event_attrs_create(lib.ph)
attrs.server_time_utc = time
ret = lib.hexchat_emit_print_attrs(lib.ph, attrs, event_name.encode(), *cargs)
lib.hexchat_event_attrs_free(lib.ph, attrs)
return ret
# TODO: this shadows itself. command should be changed to cmd
def command(command):
lib.hexchat_command(lib.ph, command.encode())
def nickcmp(string1, string2):
return lib.hexchat_nickcmp(lib.ph, string1.encode(), string2.encode())
def strip(text, length=-1, flags=3):
stripped = lib.hexchat_strip(lib.ph, text.encode(), length, flags)
ret = __decode(ffi.string(stripped))
lib.hexchat_free(lib.ph, stripped)
return ret
def get_info(name):
ret = lib.hexchat_get_info(lib.ph, name.encode())
if ret == ffi.NULL:
return None
if name in ('gtkwin_ptr', 'win_ptr'):
# Surely there is a less dumb way?
ptr = repr(ret).rsplit(' ', 1)[1][:-1]
return ptr
return __decode(ffi.string(ret))
def get_prefs(name):
string_out = ffi.new('char**')
int_out = ffi.new('int*')
_type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out)
if _type == 0:
return None
if _type == 1:
return __decode(ffi.string(string_out[0]))
if _type in (2, 3): # XXX: 3 should be a bool, but keeps API
return int_out[0]
raise AssertionError('Out of bounds pref storage')
def __cstrarray_to_list(arr):
i = 0
ret = []
while arr[i] != ffi.NULL:
ret.append(ffi.string(arr[i]))
i += 1
return ret
__FIELD_CACHE = {}
def __get_fields(name):
return __FIELD_CACHE.setdefault(name, __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name)))
__FIELD_PROPERTY_CACHE = {}
def __cached_decoded_str(string):
return __FIELD_PROPERTY_CACHE.setdefault(string, __decode(string))
def get_lists():
return [__cached_decoded_str(field) for field in __get_fields(b'lists')]
class ListItem:
def __init__(self, name):
self._listname = name
def __repr__(self):
return '<{} list item at {}>'.format(self._listname, id(self))
# done this way for speed
if sys.version_info[0] == 2:
def get_getter(name):
return ord(name[0])
else:
def get_getter(name):
return name[0]
def get_list(name):
# XXX: This function is extremely inefficient and could be interators and
# lazily loaded properties, but for API compat we stay slow
orig_name = name
name = name.encode()
if name not in __get_fields(b'lists'):
raise KeyError('list not available')
list_ = lib.hexchat_list_get(lib.ph, name)
if list_ == ffi.NULL:
return None
ret = []
fields = __get_fields(name)
def string_getter(field):
string = lib.hexchat_list_str(lib.ph, list_, field)
if string != ffi.NULL:
return __decode(ffi.string(string))
return ''
def ptr_getter(field):
if field == b'context':
ptr = lib.hexchat_list_str(lib.ph, list_, field)
ctx = ffi.cast('hexchat_context*', ptr)
return Context(ctx)
return None
getters = {
ord('s'): string_getter,
ord('i'): lambda field: lib.hexchat_list_int(lib.ph, list_, field),
ord('t'): lambda field: lib.hexchat_list_time(lib.ph, list_, field),
ord('p'): ptr_getter,
}
while lib.hexchat_list_next(lib.ph, list_) == 1:
item = ListItem(orig_name)
for _field in fields:
getter = getters.get(get_getter(_field))
if getter is not None:
field_name = _field[1:]
setattr(item, __cached_decoded_str(field_name), getter(field_name))
ret.append(item)
lib.hexchat_list_free(lib.ph, list_)
return ret
# TODO: 'command' here shadows command above, and should be renamed to cmd
def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook,
help.encode() if help is not None else ffi.NULL, hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_print(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook, hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook, hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_server(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook, hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook, hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_timer(timeout, callback, userdata=None):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_timer(lib.ph, timeout, lib._on_timer_hook, hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_unload(callback, userdata=None):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata, is_unload=True)
return id(hook)
def unhook(handle):
plugin = __get_current_plugin()
return plugin.remove_hook(handle)
def set_pluginpref(name, value):
if isinstance(value, str):
return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode()))
if isinstance(value, int):
return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value))
# XXX: This should probably raise but this keeps API
return False
def get_pluginpref(name):
name = name.encode()
string_out = ffi.new('char[512]')
if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) != 1:
return None
string = ffi.string(string_out)
# This API stores everything as a string so we have to figure out what
# its actual type was supposed to be.
if len(string) > 12: # Can't be a number
return __decode(string)
number = lib.hexchat_pluginpref_get_int(lib.ph, name)
if number == -1 and string != b'-1':
return __decode(string)
return number
def del_pluginpref(name):
return bool(lib.hexchat_pluginpref_delete(lib.ph, name.encode()))
def list_pluginpref():
prefs_str = ffi.new('char[4096]')
if lib.hexchat_pluginpref_list(lib.ph, prefs_str) == 1:
return __decode(ffi.string(prefs_str)).split(',')
return []
class Context:
def __init__(self, ctx):
self._ctx = ctx
def __eq__(self, value):
if not isinstance(value, Context):
return False
return self._ctx == value._ctx
@contextmanager
def __change_context(self):
old_ctx = lib.hexchat_get_context(lib.ph)
if not self.set():
# XXX: Behavior change, previously used wrong context
lib.hexchat_print(lib.ph, b'Context object refers to closed context, ignoring call')
return
yield
lib.hexchat_set_context(lib.ph, old_ctx)
def set(self):
# XXX: API addition, C plugin silently ignored failure
return bool(lib.hexchat_set_context(lib.ph, self._ctx))
def prnt(self, string):
with self.__change_context():
prnt(string)
def emit_print(self, event_name, *args, **kwargs):
time = kwargs.pop('time', 0) # For py2 compat
with self.__change_context():
return emit_print(event_name, *args, time=time)
def command(self, string):
with self.__change_context():
command(string)
def get_info(self, name):
with self.__change_context():
return get_info(name)
def get_list(self, name):
with self.__change_context():
return get_list(name)
def get_context():
ctx = lib.hexchat_get_context(lib.ph)
return Context(ctx)
def find_context(server=None, channel=None):
server = server.encode() if server is not None else ffi.NULL
channel = channel.encode() if channel is not None else ffi.NULL
ctx = lib.hexchat_find_context(lib.ph, server, channel)
if ctx == ffi.NULL:
return None
return Context(ctx)

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
import sys
import cffi
builder = cffi.FFI()
# hexchat-plugin.h
with open(sys.argv[1]) as f:
output = []
eat_until_endif = 0
# This is very specific to hexchat-plugin.h, it is not a cpp
for line in f:
if line.startswith('#define'):
continue
elif line.endswith('HEXCHAT_PLUGIN_H\n'):
continue
elif 'time.h' in line:
output.append('typedef int... time_t;')
elif line.startswith('#if'):
eat_until_endif += 1
elif line.startswith('#endif'):
eat_until_endif -= 1
elif eat_until_endif and '_hexchat_context' not in line:
continue
else:
output.append(line)
builder.cdef(''.join(output))
builder.embedding_api('''
extern "Python" int _on_py_command(char **, char **, void *);
extern "Python" int _on_load_command(char **, char **, void *);
extern "Python" int _on_unload_command(char **, char **, void *);
extern "Python" int _on_reload_command(char **, char **, void *);
extern "Python" int _on_say_command(char **, char **, void *);
extern "Python" int _on_command_hook(char **, char **, void *);
extern "Python" int _on_print_hook(char **, void *);
extern "Python" int _on_print_attrs_hook(char **, hexchat_event_attrs *, void *);
extern "Python" int _on_server_hook(char **, char **, void *);
extern "Python" int _on_server_attrs_hook(char **, char **, hexchat_event_attrs *, void *);
extern "Python" int _on_timer_hook(void *);
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
extern "Python" int _on_plugin_deinit(void);
static hexchat_plugin *ph;
''')
builder.set_source('_hexchat_embedded', '''
/* Python's header defines these.. */
#undef HAVE_MEMRCHR
#undef HAVE_STRINGS_H
#include "config.h"
#include "hexchat-plugin.h"
static hexchat_plugin *ph;
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
CFFI_DLLEXPORT int _on_plugin_deinit(void);
int hexchat_plugin_init(hexchat_plugin *plugin_handle,
char **name_out, char **description_out,
char **version_out, char *arg)
{
if (ph != NULL)
{
puts ("Python plugin already loaded\\n");
return 0; /* Prevent loading twice */
}
ph = plugin_handle;
return _on_plugin_init(name_out, description_out, version_out, arg, HEXCHATLIBDIR);
}
int hexchat_plugin_deinit(void)
{
int ret = _on_plugin_deinit();
ph = NULL;
return ret;
}
''')
# python.py
with open(sys.argv[2]) as f:
builder.embedding_init_code(f.read())
# python.c
builder.emit_c_code(sys.argv[3])

View File

@ -0,0 +1 @@
from _hexchat import *

View File

@ -1,13 +1,32 @@
python_opt = get_option('with-python')
if python_opt.startswith('python3')
python_dep = dependency(python_opt, version: '>= 3.3')
# Python 3.8 introduced a new -embed variant
if not python_opt.endswith('-embed')
python_dep = dependency(python_opt + '-embed', version: '>= 3.3', required: false)
if not python_dep.found()
python_dep = dependency(python_opt, version: '>= 3.3')
endif
else
python_dep = dependency(python_opt, version: '>= 3.3')
endif
else
python_dep = dependency(python_opt, version: '>= 2.7')
endif
shared_module('python', 'python.c',
dependencies: [libgio_dep, hexchat_plugin_dep, python_dep],
python3_source = custom_target('python-bindings',
input: ['../../src/common/hexchat-plugin.h', 'python.py'],
output: 'python.c',
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
)
install_data(['_hexchat.py', 'hexchat.py', 'xchat.py'],
install_dir: join_paths(get_option('libdir'), 'hexchat/python')
)
shared_module('python', python3_source,
dependencies: [hexchat_plugin_dep, python_dep],
install: true,
install_dir: plugindir,
name_prefix: '',
vs_module_defs: 'python.def'
)

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
EXPORTS
hexchat_plugin_init
hexchat_plugin_deinit
hexchat_plugin_get_info

554
plugins/python/python.py Normal file
View File

@ -0,0 +1,554 @@
from __future__ import print_function
import importlib
import os
import pydoc
import signal
import sys
import traceback
import weakref
from contextlib import contextmanager
from _hexchat_embedded import ffi, lib
if sys.version_info < (3, 0):
from io import BytesIO as HelpEater
else:
from io import StringIO as HelpEater
if not hasattr(sys, 'argv'):
sys.argv = ['<hexchat>']
VERSION = b'2.0' # Sync with hexchat.__version__
PLUGIN_NAME = ffi.new('char[]', b'Python')
PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1]))
PLUGIN_VERSION = ffi.new('char[]', VERSION)
# TODO: Constants should be screaming snake case
hexchat = None
local_interp = None
hexchat_stdout = None
plugins = set()
@contextmanager
def redirected_stdout():
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
yield
sys.stdout = hexchat_stdout
sys.stderr = hexchat_stdout
if os.getenv('HEXCHAT_LOG_PYTHON'):
def log(*args):
with redirected_stdout():
print(*args)
else:
def log(*args):
pass
class Stdout:
def __init__(self):
self.buffer = bytearray()
def write(self, string):
string = string.encode()
idx = string.rfind(b'\n')
if idx != -1:
self.buffer += string[:idx]
lib.hexchat_print(lib.ph, bytes(self.buffer))
self.buffer = bytearray(string[idx + 1:])
else:
self.buffer += string
def isatty(self):
return False
class Attribute:
def __init__(self):
self.time = 0
def __repr__(self):
return '<Attribute object at {}>'.format(id(self))
class Hook:
def __init__(self, plugin, callback, userdata, is_unload):
self.is_unload = is_unload
self.plugin = weakref.proxy(plugin)
self.callback = callback
self.userdata = userdata
self.hexchat_hook = None
self.handle = ffi.new_handle(weakref.proxy(self))
def __del__(self):
log('Removing hook', id(self))
if self.is_unload is False:
assert self.hexchat_hook is not None
lib.hexchat_unhook(lib.ph, self.hexchat_hook)
if sys.version_info[0] == 2:
def compile_file(data, filename):
return compile(data, filename, 'exec', dont_inherit=True)
def compile_line(string):
try:
return compile(string, '<string>', 'eval', dont_inherit=True)
except SyntaxError:
# For some reason `print` is invalid for eval
# This will hide any return value though
return compile(string, '<string>', 'exec', dont_inherit=True)
else:
def compile_file(data, filename):
return compile(data, filename, 'exec', optimize=2, dont_inherit=True)
def compile_line(string):
# newline appended to solve unexpected EOF issues
return compile(string + '\n', '<string>', 'single', optimize=2, dont_inherit=True)
class Plugin:
def __init__(self):
self.ph = None
self.name = ''
self.filename = ''
self.version = ''
self.description = ''
self.hooks = set()
self.globals = {
'__plugin': weakref.proxy(self),
'__name__': '__main__',
}
def add_hook(self, callback, userdata, is_unload=False):
hook = Hook(self, callback, userdata, is_unload=is_unload)
self.hooks.add(hook)
return hook
def remove_hook(self, hook):
for h in self.hooks:
if id(h) == hook:
ud = h.userdata
self.hooks.remove(h)
return ud
log('Hook not found')
return None
def loadfile(self, filename):
try:
self.filename = filename
with open(filename, 'rb') as f:
data = f.read().decode('utf-8')
compiled = compile_file(data, filename)
exec(compiled, self.globals)
try:
self.name = self.globals['__module_name__']
except KeyError:
lib.hexchat_print(lib.ph, b'Failed to load module: __module_name__ must be set')
return False
self.version = self.globals.get('__module_version__', '')
self.description = self.globals.get('__module_description__', '')
self.ph = lib.hexchat_plugingui_add(lib.ph, filename.encode(), self.name.encode(),
self.description.encode(), self.version.encode(), ffi.NULL)
except Exception as e:
lib.hexchat_print(lib.ph, 'Failed to load module: {}'.format(e).encode())
traceback.print_exc()
return False
return True
def __del__(self):
log('unloading', self.filename)
for hook in self.hooks:
if hook.is_unload is True:
try:
hook.callback(hook.userdata)
except Exception as e:
log('Failed to run hook:', e)
traceback.print_exc()
del self.hooks
if self.ph is not None:
lib.hexchat_plugingui_remove(lib.ph, self.ph)
if sys.version_info[0] == 2:
def __decode(string):
return string
else:
def __decode(string):
return string.decode()
# There can be empty entries between non-empty ones so find the actual last value
def wordlist_len(words):
for i in range(31, 0, -1):
if ffi.string(words[i]):
return i
return 0
def create_wordlist(words):
size = wordlist_len(words)
return [__decode(ffi.string(words[i])) for i in range(1, size + 1)]
# This function only exists for compat reasons with the C plugin
# It turns the word list from print hooks into a word_eol list
# This makes no sense to do...
def create_wordeollist(words):
words = reversed(words)
accum = None
ret = []
for word in words:
if accum is None:
accum = word
elif word:
last = accum
accum = ' '.join((word, last))
ret.insert(0, accum)
return ret
def to_cb_ret(value):
if value is None:
return 0
return int(value)
@ffi.def_extern()
def _on_command_hook(word, word_eol, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordlist(word_eol)
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
@ffi.def_extern()
def _on_print_hook(word, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordeollist(word)
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
@ffi.def_extern()
def _on_print_attrs_hook(word, attrs, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordeollist(word)
attr = Attribute()
attr.time = attrs.server_time_utc
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
@ffi.def_extern()
def _on_server_hook(word, word_eol, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordlist(word_eol)
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
@ffi.def_extern()
def _on_server_attrs_hook(word, word_eol, attrs, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordlist(word_eol)
attr = Attribute()
attr.time = attrs.server_time_utc
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
@ffi.def_extern()
def _on_timer_hook(userdata):
hook = ffi.from_handle(userdata)
if hook.callback(hook.userdata) == True:
return 1
hook.is_unload = True # Don't unhook
for h in hook.plugin.hooks:
if h == hook:
hook.plugin.hooks.remove(h)
break
return 0
@ffi.def_extern(error=3)
def _on_say_command(word, word_eol, userdata):
channel = ffi.string(lib.hexchat_get_info(lib.ph, b'channel'))
if channel == b'>>python<<':
python = ffi.string(word_eol[1])
lib.hexchat_print(lib.ph, b'>>> ' + python)
exec_in_interp(__decode(python))
return 1
return 0
def load_filename(filename):
filename = os.path.expanduser(filename)
if not os.path.isabs(filename):
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
filename = os.path.join(configdir, 'addons', filename)
if filename and not any(plugin.filename == filename for plugin in plugins):
plugin = Plugin()
if plugin.loadfile(filename):
plugins.add(plugin)
return True
return False
def unload_name(name):
if name:
for plugin in plugins:
if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)):
plugins.remove(plugin)
return True
return False
def reload_name(name):
if name:
for plugin in plugins:
if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)):
filename = plugin.filename
plugins.remove(plugin)
return load_filename(filename)
return False
@contextmanager
def change_cwd(path):
old_cwd = os.getcwd()
os.chdir(path)
yield
os.chdir(old_cwd)
def autoload():
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
addondir = os.path.join(configdir, 'addons')
try:
with change_cwd(addondir): # Maintaining old behavior
for f in os.listdir(addondir):
if f.endswith('.py'):
log('Autoloading', f)
# TODO: Set cwd
load_filename(os.path.join(addondir, f))
except FileNotFoundError as e:
log('Autoload failed', e)
def list_plugins():
if not plugins:
lib.hexchat_print(lib.ph, b'No python modules loaded')
return
tbl_headers = [b'Name', b'Version', b'Filename', b'Description']
tbl = [
tbl_headers,
[(b'-' * len(s)) for s in tbl_headers]
]
for plugin in plugins:
basename = os.path.basename(plugin.filename).encode()
name = plugin.name.encode()
version = plugin.version.encode() if plugin.version else b'<none>'
description = plugin.description.encode() if plugin.description else b'<none>'
tbl.append((name, version, basename, description))
column_sizes = [
max(len(item) for item in column)
for column in zip(*tbl)
]
for row in tbl:
lib.hexchat_print(lib.ph, b' '.join(item.ljust(column_sizes[i])
for i, item in enumerate(row)))
lib.hexchat_print(lib.ph, b'')
def exec_in_interp(python):
global local_interp
if not python:
return
if local_interp is None:
local_interp = Plugin()
local_interp.locals = {}
local_interp.globals['hexchat'] = hexchat
code = compile_line(python)
try:
ret = eval(code, local_interp.globals, local_interp.locals)
if ret is not None:
lib.hexchat_print(lib.ph, '{}'.format(ret).encode())
except Exception as e:
traceback.print_exc(file=hexchat_stdout)
@ffi.def_extern()
def _on_load_command(word, word_eol, userdata):
filename = ffi.string(word[2])
if filename.endswith(b'.py'):
load_filename(__decode(filename))
return 3
return 0
@ffi.def_extern()
def _on_unload_command(word, word_eol, userdata):
filename = ffi.string(word[2])
if filename.endswith(b'.py'):
unload_name(__decode(filename))
return 3
return 0
@ffi.def_extern()
def _on_reload_command(word, word_eol, userdata):
filename = ffi.string(word[2])
if filename.endswith(b'.py'):
reload_name(__decode(filename))
return 3
return 0
@ffi.def_extern(error=3)
def _on_py_command(word, word_eol, userdata):
subcmd = __decode(ffi.string(word[2])).lower()
if subcmd == 'exec':
python = __decode(ffi.string(word_eol[3]))
exec_in_interp(python)
elif subcmd == 'load':
filename = __decode(ffi.string(word[3]))
load_filename(filename)
elif subcmd == 'unload':
name = __decode(ffi.string(word[3]))
if not unload_name(name):
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
elif subcmd == 'reload':
name = __decode(ffi.string(word[3]))
if not reload_name(name):
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
elif subcmd == 'console':
lib.hexchat_command(lib.ph, b'QUERY >>python<<')
elif subcmd == 'list':
list_plugins()
elif subcmd == 'about':
lib.hexchat_print(lib.ph, b'HexChat Python interface version ' + VERSION)
else:
lib.hexchat_command(lib.ph, b'HELP PY')
return 3
@ffi.def_extern()
def _on_plugin_init(plugin_name, plugin_desc, plugin_version, arg, libdir):
global hexchat
global hexchat_stdout
signal.signal(signal.SIGINT, signal.SIG_DFL)
plugin_name[0] = PLUGIN_NAME
plugin_desc[0] = PLUGIN_DESC
plugin_version[0] = PLUGIN_VERSION
try:
libdir = __decode(ffi.string(libdir))
modpath = os.path.join(libdir, '..', 'python')
sys.path.append(os.path.abspath(modpath))
hexchat = importlib.import_module('hexchat')
except (UnicodeDecodeError, ImportError) as e:
lib.hexchat_print(lib.ph, b'Failed to import module: ' + repr(e).encode())
return 0
hexchat_stdout = Stdout()
sys.stdout = hexchat_stdout
sys.stderr = hexchat_stdout
pydoc.help = pydoc.Helper(HelpEater(), HelpEater())
lib.hexchat_hook_command(lib.ph, b'', 0, lib._on_say_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'LOAD', 0, lib._on_load_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'UNLOAD', 0, lib._on_unload_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'RELOAD', 0, lib._on_reload_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'PY', 0, lib._on_py_command, b'''Usage: /PY LOAD <filename>
UNLOAD <filename|name>
RELOAD <filename|name>
LIST
EXEC <command>
CONSOLE
ABOUT''', ffi.NULL)
lib.hexchat_print(lib.ph, b'Python interface loaded')
autoload()
return 1
@ffi.def_extern()
def _on_plugin_deinit():
global local_interp
global hexchat
global hexchat_stdout
global plugins
plugins = set()
local_interp = None
hexchat = None
hexchat_stdout = None
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
pydoc.help = pydoc.Helper()
for mod in ('_hexchat', 'hexchat', 'xchat', '_hexchat_embedded'):
try:
del sys.modules[mod]
except KeyError:
pass
return 1

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
# HexChat Python Module Style Guide
(This is a work in progress).
## General rules
- PEP8 as general fallback recommendations
- Max line length: 120
- Avoid overcomplex compound statements. i.e. dont do this: `somevar = x if x == y else z if a == b and c == b else x`
## Indentation style
### Multi-line functions
```python
foo(really_long_arg_1,
really_long_arg_2)
```
### Mutli-line lists/dicts
```python
foo = {
'bar': 'baz',
}
```

1
plugins/python/xchat.py Normal file
View File

@ -0,0 +1 @@
from _hexchat import *

View File

@ -13,13 +13,13 @@ sysinfo_includes = []
sysinfo_cargs = []
system = host_machine.system()
if system == 'linux' or system == 'darwin'
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'darwin' or system == 'freebsd'
sysinfo_includes += 'shared'
sysinfo_sources += [
'shared/df.c'
]
if system == 'linux'
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'freebsd'
libpci = dependency('libpci', required: false, method: 'pkg-config')
if libpci.found()
sysinfo_deps += libpci
@ -57,4 +57,5 @@ shared_module('sysinfo', sysinfo_sources,
install: true,
install_dir: plugindir,
name_prefix: '',
vs_module_defs: 'sysinfo.def',
)

View File

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

View File

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

View File

@ -269,6 +269,16 @@ int xs_parse_meminfo(unsigned long long *mem_tot, unsigned long long *mem_free,
return 0;
}
static void strip_quotes(char *string)
{
size_t len = strlen(string);
if (string[len - 1] == '"')
string[--len] = '\0';
if (string[0] == '"')
memmove(string, string + 1, len);
}
int xs_parse_distro(char *name)
{
FILE *fp = NULL;
@ -320,6 +330,20 @@ int xs_parse_distro(char *name)
else
g_snprintf(buffer, bsize, "Gentoo Linux %s", keywords);
}
else if((fp = fopen("/etc/os-release", "r")) != NULL)
{
char name[bsize], version[bsize];
strcpy(name, "?");
strcpy(version, "?");
while(fgets(buffer, bsize, fp) != NULL)
{
find_match_char(buffer, "NAME=", name);
find_match_char(buffer, "VERSION=", version);
}
strip_quotes(name);
strip_quotes(version);
g_snprintf(buffer, bsize, "%s %s", name, version);
}
else
g_snprintf(buffer, bsize, "Unknown Distro");
if(fp != NULL)

View File

@ -142,7 +142,7 @@ void pci_find_fullname(char *fullname, char *vendor, char *device)
{
position = strstr(buffer, vendor);
position += 6;
strncpy(vendorname, position, bsize/2);
g_strlcpy(vendorname, position, sizeof (vendorname));
position = strstr(vendorname, "\n");
*(position) = '\0';
break;
@ -154,7 +154,7 @@ void pci_find_fullname(char *fullname, char *vendor, char *device)
{
position = strstr(buffer, device);
position += 6;
strncpy(devicename, position, bsize/2);
g_strlcpy(devicename, position, sizeof (devicename));
position = strstr(devicename, " (");
if (position == NULL)
position = strstr(devicename, "\n");

View File

@ -5,4 +5,5 @@ shared_module('upd', 'upd.c',
install: true,
install_dir: plugindir,
name_prefix: '',
vs_module_defs: 'upd.def',
)

View File

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

View File

@ -3,4 +3,5 @@ shared_module('winamp', 'winamp.c',
install: true,
install_dir: plugindir,
name_prefix: '',
vs_module_defs: 'winamp.def',
)

View File

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

631
po/af.po

File diff suppressed because it is too large Load Diff

631
po/am.po

File diff suppressed because it is too large Load Diff

626
po/ast.po

File diff suppressed because it is too large Load Diff

635
po/az.po

File diff suppressed because it is too large Load Diff

635
po/be.po

File diff suppressed because it is too large Load Diff

635
po/bg.po

File diff suppressed because it is too large Load Diff

661
po/ca.po

File diff suppressed because it is too large Load Diff

1131
po/cs.po

File diff suppressed because it is too large Load Diff

651
po/da.po

File diff suppressed because it is too large Load Diff

835
po/de.po

File diff suppressed because it is too large Load Diff

635
po/el.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1808
po/es.po

File diff suppressed because it is too large Load Diff

625
po/et.po

File diff suppressed because it is too large Load Diff

625
po/eu.po

File diff suppressed because it is too large Load Diff

626
po/fi.po

File diff suppressed because it is too large Load Diff

635
po/fr.po

File diff suppressed because it is too large Load Diff

625
po/gl.po

File diff suppressed because it is too large Load Diff

635
po/gu.po

File diff suppressed because it is too large Load Diff

636
po/hi.po

File diff suppressed because it is too large Load Diff

628
po/hu.po

File diff suppressed because it is too large Load Diff

627
po/id.po

File diff suppressed because it is too large Load Diff

635
po/it.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

627
po/kn.po

File diff suppressed because it is too large Load Diff

712
po/ko.po

File diff suppressed because it is too large Load Diff

667
po/lt.po

File diff suppressed because it is too large Load Diff

635
po/lv.po

File diff suppressed because it is too large Load Diff

View File

@ -1 +1,9 @@
i18n.gettext('hexchat', preset: 'glib')
validate_translations = find_program('validate-textevent-translations')
test('Validate translations', validate_translations,
args: [
join_paths(meson.current_source_dir(), 'LINGUAS'),
meson.current_source_dir(),
]
)

637
po/mk.po

File diff suppressed because it is too large Load Diff

633
po/ml.po

File diff suppressed because it is too large Load Diff

635
po/ms.po

File diff suppressed because it is too large Load Diff

628
po/nb.po

File diff suppressed because it is too large Load Diff

635
po/nl.po

File diff suppressed because it is too large Load Diff

635
po/no.po

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More