summaryrefslogtreecommitdiff
path: root/plugins/xC
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/xC')
-rw-r--r--plugins/xC/auto-rejoin.lua48
-rw-r--r--plugins/xC/censor.lua90
-rw-r--r--plugins/xC/fancy-prompt.lua113
-rw-r--r--plugins/xC/last-fm.lua178
-rw-r--r--plugins/xC/ping-timeout.lua32
-rw-r--r--plugins/xC/prime.lua68
-rw-r--r--plugins/xC/slack.lua147
-rw-r--r--plugins/xC/thin-cursor.lua27
-rw-r--r--plugins/xC/utm-filter.lua66
9 files changed, 769 insertions, 0 deletions
diff --git a/plugins/xC/auto-rejoin.lua b/plugins/xC/auto-rejoin.lua
new file mode 100644
index 0000000..f42fb2e
--- /dev/null
+++ b/plugins/xC/auto-rejoin.lua
@@ -0,0 +1,48 @@
+--
+-- auto-rejoin.lua: join back automatically when someone kicks you
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local timeout
+xC.setup_config {
+ timeout = {
+ type = "integer",
+ comment = "auto rejoin timeout",
+ default = "0",
+
+ on_change = function (v)
+ timeout = v
+ end,
+ validate = function (v)
+ if v < 0 then error ("timeout must not be negative", 0) end
+ end,
+ },
+}
+
+async, await = xC.async, coroutine.yield
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ if msg.command ~= "KICK" then return line end
+
+ local who = msg.prefix:match ("^[^!]*")
+ local channel, whom = table.unpack (msg.params)
+ if who ~= whom and whom == server.user.nickname then
+ async.go (function ()
+ await (async.timer_ms (timeout * 1000))
+ server:send ("JOIN " .. channel)
+ end)
+ end
+ return line
+end)
diff --git a/plugins/xC/censor.lua b/plugins/xC/censor.lua
new file mode 100644
index 0000000..49cab5b
--- /dev/null
+++ b/plugins/xC/censor.lua
@@ -0,0 +1,90 @@
+--
+-- censor.lua: black out certain users' messages
+--
+-- Copyright (c) 2016 - 2021, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local to_pattern = function (mask)
+ if not mask:match ("!") then mask = mask .. "!*" end
+ if not mask:match ("@") then mask = mask .. "@*" end
+
+ -- That is, * acts like a wildcard, otherwise everything is escaped
+ return "^" .. mask:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
+ :gsub ("%*", ".*") .. "$"
+end
+
+local patterns = {}
+local read_masks = function (v)
+ patterns = {}
+ local add = function (who, where)
+ local channels = patterns[who] or {}
+ table.insert (channels, where)
+ patterns[who] = channels
+ end
+ for item in v:lower ():gmatch ("[^,]+") do
+ local who, where = item:match ("^([^/]+)/*(.*)")
+ if who then add (to_pattern (who), where == "" or where) end
+ end
+end
+
+local quote
+xC.setup_config {
+ masks = {
+ type = "string_array",
+ default = "\"\"",
+ comment = "user masks (optionally \"/#channel\") to censor",
+ on_change = read_masks
+ },
+ quote = {
+ type = "string",
+ default = "\"\\x0301,01\"",
+ comment = "formatting prefix for censored messages",
+ on_change = function (v) quote = v end
+ },
+}
+
+local decolor = function (text)
+ local rebuilt, last = {""}, 1
+ for start in text:gmatch ('()\x03') do
+ table.insert (rebuilt, text:sub (last, start - 1))
+ local sub = text:sub (start + 1)
+ last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
+ end
+ return table.concat (rebuilt) .. text:sub (last)
+end
+
+local censor = function (line)
+ -- Taking a shortcut to avoid lengthy message reassembly
+ local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ local ctcp, rest = text:match ("^(\x01%g+ )(.*)")
+ text = ctcp and ctcp .. quote .. decolor (rest) or quote .. decolor (text)
+ return start .. text
+end
+
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ if msg.command ~= "PRIVMSG" then return line end
+
+ local channel = msg.params[1]:lower ()
+ for who, where in pairs (patterns) do
+ if msg.prefix:lower ():match (who) then
+ for _, x in pairs (where) do
+ if x == true or x == channel then
+ return censor (line)
+ end
+ end
+ end
+ end
+ return line
+end)
diff --git a/plugins/xC/fancy-prompt.lua b/plugins/xC/fancy-prompt.lua
new file mode 100644
index 0000000..0b7000c
--- /dev/null
+++ b/plugins/xC/fancy-prompt.lua
@@ -0,0 +1,113 @@
+--
+-- fancy-prompt.lua: the fancy multiline prompt you probably want
+--
+-- Copyright (c) 2016 - 2022, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+-- Beware that it is a hack and only goes about 90% of the way, which is why
+-- this functionality is only available as a plugin in the first place
+-- (well, and also for customizability).
+--
+-- The biggest problem is that the way we work with Readline is incompatible
+-- with multiline prompts, and normal newlines just don't work. This is being
+-- circumvented by using an overflowing single-line prompt with a specially
+-- crafted character in the rightmost column that prevents the bar's background
+-- from spilling all over the last line.
+--
+-- There is also a problem with C-r search rendering not clearing out the
+-- background but to really fix that mode, we'd have to fully reimplement it
+-- since its alternative prompt very often gets overriden by accident anyway.
+
+xC.hook_prompt (function (hook)
+ local current = xC.current_buffer
+ local chan = current.channel
+ local s = current.server
+
+ local bg_color = "255"
+ local current_n = 0
+ local active = ""
+ for i, buffer in ipairs (xC.buffers) do
+ if buffer == current then
+ current_n = i
+ elseif buffer.new_messages_count ~= buffer.new_unimportant_count then
+ active = active .. ","
+ if buffer.highlighted then
+ active = active .. "!"
+ bg_color = "224"
+ end
+ active = active .. i
+ end
+ end
+ local x = current_n .. ":" .. current.name
+ if chan and chan.users_len ~= 0 then
+ local params = ""
+ for mode, param in pairs (chan.param_modes) do
+ params = params .. " +" .. mode .. " " .. param
+ end
+ local modes = chan.no_param_modes .. params:sub (3)
+ if modes ~= "" then
+ x = x .. "(+" .. modes .. ")"
+ end
+ x = x .. "{" .. chan.users_len .. "}"
+ end
+ if current.hide_unimportant then
+ x = x .. "<H>"
+ end
+ if active ~= "" then
+ x = x .. " (" .. active:sub (2) .. ")"
+ end
+
+ -- Readline 7.0.003 seems to be broken and completely corrupts the prompt.
+ -- However 8.0.004 seems to be fine with these, as is libedit 20191231-3.1.
+ --x = x:gsub("[\128-\255]", "?")
+
+ -- Align to the terminal's width and apply formatting, including the hack.
+ local lines, cols = xC.get_screen_size ()
+ local trailing, width = " ", xC.measure (x)
+ while cols > 0 and width >= cols do
+ x = x:sub (1, utf8.offset (x, -1) - 1)
+ trailing, width = ">", xC.measure (x)
+ end
+
+ x = "\x01\x1b[0;4;1;38;5;16m\x1b[48;5;" .. bg_color .. "m\x02" ..
+ x .. string.rep (" ", cols - width - 1) ..
+ "\x01\x1b[0;4;1;7;38;5;" .. bg_color .. "m\x02" ..
+ trailing .. "\x01\x1b[0;1m\x02"
+
+ local user_prefix = function (chan, user)
+ for i, chan_user in ipairs (chan.users) do
+ if chan_user.user == user then return chan_user.prefixes end
+ end
+ return ""
+ end
+ if s then
+ x = x .. "["
+ local state = s.state
+ if state == "disconnected" or state == "connecting" then
+ x = x .. "(" .. state .. ")"
+ elseif state ~= "registered" then
+ x = x .. "(unregistered)"
+ else
+ local user, modes = s.user, s.user_mode
+ if chan then x = x .. user_prefix (chan, user) end
+ x = x .. user.nickname
+ if modes ~= "" then x = x .. "(" .. modes .. ")" end
+ end
+ x = x .. "] "
+ else
+ -- There needs to be at least one character so that the cursor
+ -- doesn't get damaged by our hack in that last column
+ x = x .. "> "
+ end
+ return x
+end)
diff --git a/plugins/xC/last-fm.lua b/plugins/xC/last-fm.lua
new file mode 100644
index 0000000..3bdfed2
--- /dev/null
+++ b/plugins/xC/last-fm.lua
@@ -0,0 +1,178 @@
+--
+-- last-fm.lua: "now playing" feature using the last.fm API
+--
+-- Dependencies: lua-cjson (from luarocks e.g.)
+--
+-- I call this style closure-oriented programming
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local cjson = require "cjson"
+
+-- Setup configuration to load last.fm API credentials from
+local user, api_key
+xC.setup_config {
+ user = {
+ type = "string",
+ comment = "last.fm username",
+ on_change = function (v) user = v end
+ },
+ api_key = {
+ type = "string",
+ comment = "last.fm API key",
+ on_change = function (v) api_key = v end
+ },
+}
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+-- Generic error reporting
+local report_error = function (buffer, error)
+ buffer:log ("last-fm error: " .. error)
+end
+
+-- Process data return by the server and extract the now playing song
+local process = function (buffer, data, action)
+ -- There's no reasonable Lua package to parse HTTP that I could find
+ local s, e, v, status, message = string.find (data, "(%S+) (%S+) .+\r\n")
+ if not s then return "server returned unexpected data" end
+ if status ~= "200" then return status .. " " .. message end
+
+ local s, e = string.find (data, "\r\n\r\n")
+ if not s then return "server returned unexpected data" end
+
+ local parser = cjson.new ()
+ data = parser.decode (string.sub (data, e + 1))
+ if not data.recenttracks or not data.recenttracks.track then
+ return "invalid response" end
+
+ -- Need to make some sense of the XML automatically converted to JSON
+ local text_of = function (node)
+ if type (node) ~= "table" then return node end
+ return node["#text"] ~= "" and node["#text"] or nil
+ end
+
+ local name, artist, album
+ for i, track in ipairs (data.recenttracks.track) do
+ if track["@attr"] and track["@attr"].nowplaying then
+ if track.name then name = text_of (track.name) end
+ if track.artist then artist = text_of (track.artist) end
+ if track.album then album = text_of (track.album) end
+ end
+ end
+
+ if not name then
+ action (false)
+ else
+ local np = "\"" .. name .. "\""
+ if artist then np = np .. " by " .. artist end
+ if album then np = np .. " from " .. album end
+ action (np)
+ end
+end
+
+-- Set up the connection and make the request
+local on_connected = function (buffer, c, host, action)
+ -- Buffer data in the connection object
+ c.data = ""
+ c.on_data = function (data)
+ c.data = c.data .. data
+ end
+
+ -- And process it after we receive everything
+ c.on_eof = function ()
+ error = process (buffer, c.data, action)
+ if error then report_error (buffer, error) end
+ c:close ()
+ end
+ c.on_error = function (e)
+ report_error (buffer, e)
+ end
+
+ -- Make the unencrypted HTTP request
+ local url = "/2.0/?method=user.getrecenttracks&user=" .. user ..
+ "&limit=1&api_key=" .. api_key .. "&format=json"
+ c:send ("GET " .. url .. " HTTP/1.1\r\n")
+ c:send ("User-agent: last-fm.lua\r\n")
+ c:send ("Host: " .. host .. "\r\n")
+ c:send ("Connection: close\r\n")
+ c:send ("\r\n")
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+-- Avoid establishing more than one connection at a time
+local running
+
+-- Initiate a connection to last.fm servers
+async, await = xC.async, coroutine.yield
+local make_request = function (buffer, action)
+ if not user or not api_key then
+ report_error (buffer, "configuration is incomplete")
+ return
+ end
+
+ if running then running:cancel () end
+ running = async.go (function ()
+ local c, host, e = await (async.dial ("ws.audioscrobbler.com", 80))
+ if e then
+ report_error (buffer, e)
+ else
+ on_connected (buffer, c, host, action)
+ end
+ running = nil
+ end)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+local now_playing
+
+local tell_song = function (buffer)
+ if now_playing == nil then
+ buffer:log ("last-fm: I don't know what you're listening to")
+ elseif not now_playing then
+ buffer:log ("last-fm: not playing anything right now")
+ else
+ buffer:log ("last-fm: now playing: " .. now_playing)
+ end
+end
+
+local send_song = function (buffer)
+ if not now_playing then
+ tell_song (buffer)
+ else
+ buffer:execute ("/me is listening to " .. now_playing)
+ end
+end
+
+-- Hook input to simulate new commands
+xC.hook_input (function (hook, buffer, input)
+ if input == "/np" then
+ make_request (buffer, function (np)
+ now_playing = np
+ send_song (buffer)
+ end)
+ elseif input == "/np?" then
+ make_request (buffer, function (np)
+ now_playing = np
+ tell_song (buffer)
+ end)
+ elseif input == "/np!" then
+ send_song (buffer)
+ else
+ return input
+ end
+end)
diff --git a/plugins/xC/ping-timeout.lua b/plugins/xC/ping-timeout.lua
new file mode 100644
index 0000000..c455c57
--- /dev/null
+++ b/plugins/xC/ping-timeout.lua
@@ -0,0 +1,32 @@
+--
+-- ping-timeout.lua: ping timeout readability enhancement plugin
+--
+-- Copyright (c) 2015 - 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ local start, timeout = line:match ("^(.* :Ping timeout:) (%d+) seconds$")
+ if msg.command ~= "QUIT" or not start then
+ return line
+ end
+
+ local minutes = timeout // 60
+ if minutes == 0 then
+ return line
+ end
+
+ local seconds = timeout % 60
+ return ("%s %d minutes, %d seconds"):format (start, minutes, seconds)
+end)
diff --git a/plugins/xC/prime.lua b/plugins/xC/prime.lua
new file mode 100644
index 0000000..23740ee
--- /dev/null
+++ b/plugins/xC/prime.lua
@@ -0,0 +1,68 @@
+--
+-- prime.lua: highlight prime numbers in messages
+--
+-- Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local smallest, highlight = 0, "\x1f"
+xC.setup_config {
+ smallest = {
+ type = "integer",
+ default = "0",
+ comment = "smallest number to scan for primality",
+ on_change = function (v) smallest = math.max (v, 2) end
+ },
+ highlight = {
+ type = "string",
+ default = "\"\\x1f\"",
+ comment = "the attribute to use for highlights",
+ on_change = function (v) highlight = v end
+ },
+}
+
+-- The prime test is actually very fast, so there is no DoS concern
+local do_intercolour = function (text)
+ return tostring (text:gsub ("%f[%w_]%d+", function (n)
+ if tonumber (n) < smallest then return nil end
+ for i = 2, n ^ (1 / 2) do if (n % i) == 0 then return nil end end
+ return highlight .. n .. highlight
+ end))
+end
+
+local do_interlink = function (text)
+ local rebuilt, last = {""}, 1
+ for start in text:gmatch ('()\x03') do
+ table.insert (rebuilt, do_intercolour (text:sub (last, start - 1)))
+ local sub = text:sub (start + 1)
+ last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
+ table.insert (rebuilt, text:sub (start, last - 1))
+ end
+ return table.concat (rebuilt) .. do_intercolour (text:sub (last))
+end
+
+local do_message = function (text)
+ local rebuilt, last = {""}, 1
+ for run, link, endpos in text:gmatch ('(.-)(%f[%g]https?://%g+)()') do
+ last = endpos
+ table.insert (rebuilt, do_interlink (run) .. link)
+ end
+ return table.concat (rebuilt) .. do_interlink (text:sub (last))
+end
+
+-- XXX: sadly it won't typically highlight primes in our own messages,
+-- unless IRCv3 echo-message is on
+xC.hook_irc (function (hook, server, line)
+ local start, message = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ return message and start .. do_message (message) or line
+end)
diff --git a/plugins/xC/slack.lua b/plugins/xC/slack.lua
new file mode 100644
index 0000000..c1a08de
--- /dev/null
+++ b/plugins/xC/slack.lua
@@ -0,0 +1,147 @@
+--
+-- slack.lua: try to fix up UX when using the Slack IRC gateway
+--
+-- Copyright (c) 2017, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local servers = {}
+local read_servers = function (v)
+ servers = {}
+ for name in v:lower ():gmatch "[^,]+" do
+ servers[name] = true
+ end
+end
+
+-- This is a reverse list of Slack's automatic emoji, noseless forms
+local unemojify, emoji, emoji_default = false, {}, {
+ heart = "<3",
+ broken_heart = "</3",
+ sunglasses = "8)",
+ anguished = "D:",
+ cry = ":'(",
+ monkey_face = ":o)",
+ kiss = ":*",
+ smiley = "=)",
+ smile = ":D",
+ wink = ";)",
+ laughing = ":>",
+ neutral_face = ":|",
+ open_mouth = ":o",
+ angry = ">:(",
+ slightly_smiling_face = ":)",
+ disappointed = ":(",
+ confused = ":/",
+ stuck_out_tongue = ":p",
+ stuck_out_tongue_winking_eye = ";p",
+}
+local load_emoji = function (extra)
+ emoji = {}
+ for k, v in pairs (emoji_default) do emoji[k] = v end
+ for k, v in extra:gmatch "([^,]+) ([^,]+)" do emoji[k] = v end
+end
+
+xC.setup_config {
+ servers = {
+ type = "string_array",
+ default = "\"\"",
+ comment = "list of server names that are Slack IRC gateways",
+ on_change = read_servers
+ },
+ unemojify = {
+ type = "boolean",
+ default = "true",
+ comment = "convert emoji to normal ASCII emoticons",
+ on_change = function (v) unemojify = v end
+ },
+ extra_emoji = {
+ type = "string_array",
+ default = "\"grinning :)),joy :'),innocent o:),persevere >_<\"",
+ comment = "overrides or extra emoji for unemojify",
+ on_change = function (v) load_emoji (v) end
+ }
+}
+
+-- We can handle external messages about what we've supposedly sent just fine,
+-- so let's get rid of that "[username] some message sent from the web UI" crap
+xC.hook_irc (function (hook, server, line)
+ local msg, us = xC.parse (line), server.user
+ if not servers[server.name] or msg.command ~= "PRIVMSG" or not us
+ or msg.params[1]:lower () ~= us.nickname:lower () then return line end
+
+ -- Taking a shortcut to avoid lengthy message reassembly
+ local quoted_nick = us.nickname:gsub ("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")
+ local text = line:match ("^.- PRIVMSG .- :%[" .. quoted_nick .. "%] (.*)$")
+ if not text then return line end
+ return ":" .. us.nickname .. "!" .. server.irc_user_host .. " PRIVMSG "
+ .. msg.prefix:match "^[^!@]*" .. " :" .. text
+end)
+
+-- Unfuck emoji and :nick!nick@irc.tinyspeck.com MODE #channel +v nick : active
+xC.hook_irc (function (hook, server, line)
+ if not servers[server.name] then return line end
+ if unemojify then
+ local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ if start then return start .. text:gsub (":([a-z_]+):", function (name)
+ if emoji[name] then return emoji[name] end
+ return ":" .. name .. ":"
+ end) end
+ end
+ return line:gsub ("^(:%S+ MODE .+) : .*", "%1")
+end)
+
+-- The gateway simply ignores the NAMES command altogether
+xC.hook_input (function (hook, buffer, input)
+ if not buffer.channel or not servers[buffer.server.name]
+ or not input:match "^/names%s*$" then return input end
+
+ local users = buffer.channel.users
+ table.sort (users, function (a, b)
+ if a.prefixes > b.prefixes then return true end
+ if a.prefixes < b.prefixes then return false end
+ return a.user.nickname < b.user.nickname
+ end)
+
+ local names = "Users on " .. buffer.channel.name .. ":"
+ for i, chan_user in ipairs (users) do
+ names = names .. " " .. chan_user.prefixes .. chan_user.user.nickname
+ end
+ buffer:log (names)
+end)
+
+xC.hook_completion (function (hook, data, word)
+ local chan = xC.current_buffer.channel
+ local server = xC.current_buffer.server
+ if not chan or not servers[server.name] then return end
+
+ -- In /commands there is typically no desire at all to add the at sign
+ if data.location == 1 and data.words[1]:match "^/" then return end
+
+ -- Handle both when the at sign is already there and when it is not
+ local needle = word:gsub ("^@", ""):lower ()
+
+ local t = {}
+ local try = function (name)
+ if data.location == 0 then name = name .. ":" end
+ if name:sub (1, #needle):lower () == needle then
+ table.insert (t, "@" .. name)
+ end
+ end
+ for _, chan_user in ipairs (chan.users) do
+ try (chan_user.user.nickname)
+ end
+ for _, special in ipairs { "channel", "here" } do
+ try (special)
+ end
+ return t
+end)
diff --git a/plugins/xC/thin-cursor.lua b/plugins/xC/thin-cursor.lua
new file mode 100644
index 0000000..d0fbf38
--- /dev/null
+++ b/plugins/xC/thin-cursor.lua
@@ -0,0 +1,27 @@
+--
+-- thin-cursor.lua: set a thin cursor
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+-- If tmux doesn't work, add the following to its configuration:
+-- set -as terminal-overrides ',*:Ss=\E[%p1%d q:Se=\E[2 q'
+-- Change the "2" as per http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+
+local out = io.output ()
+out:write ("\x1b[6 q"):flush ()
+
+-- By registering a global variable, we get notified about plugin unload
+x = setmetatable ({}, { __gc = function ()
+ out:write ("\x1b[2 q"):flush ()
+end })
diff --git a/plugins/xC/utm-filter.lua b/plugins/xC/utm-filter.lua
new file mode 100644
index 0000000..b708c12
--- /dev/null
+++ b/plugins/xC/utm-filter.lua
@@ -0,0 +1,66 @@
+--
+-- utm-filter.lua: filter out Google Analytics bullshit etc. from URLs
+--
+-- Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+-- A list of useless URL parameters that don't affect page function
+local banned = {
+ gclid = 1,
+
+ -- Alas, Facebook no longer uses this parameter, see:
+ -- https://news.ycombinator.com/item?id=32117489
+ fbclid = 1,
+
+ utm_source = 1,
+ utm_medium = 1,
+ utm_term = 1,
+ utm_content = 1,
+ utm_campaign = 1,
+}
+
+-- Go through a parameter list and throw out any banned elements
+local do_args = function (args)
+ local filtered = {}
+ for part in args:gmatch ("[^&]+") do
+ if not banned[part:match ("^[^=]*")] then
+ table.insert (filtered, part)
+ end
+ end
+ return table.concat (filtered, "&")
+end
+
+-- Filter parameters in both the query and the fragment part of an URL
+local do_single_url = function (url)
+ return url:gsub ('^([^?#]*)%?([^#]*)', function (start, query)
+ local clean = do_args (query)
+ return #clean > 0 and start .. "?" .. clean or start
+ end, 1):gsub ('^([^#]*)#(.*)', function (start, fragment)
+ local clean = do_args (fragment)
+ return #clean > 0 and start .. "#" .. clean or start
+ end, 1)
+end
+
+local do_text = function (text)
+ return text:gsub ('%f[%g]https?://%g+', do_single_url)
+end
+
+xC.hook_irc (function (hook, server, line)
+ local start, message = line:match ("^(.* :)(.*)$")
+ return message and start .. do_text (message) or line
+end)
+
+xC.hook_input (function (hook, buffer, input)
+ return do_text (input)
+end)