├── .gitattributes ├── Exaile └── growl │ ├── PLUGININFO │ └── __init__.py ├── HexChat ├── banhelper.lua ├── bansearch.py ├── bookmarks.py ├── buffextras.lua ├── closeondisc.py ├── duplicates │ ├── banhelper.py │ ├── highlight.py │ ├── logpm.py │ ├── mymsg.py │ ├── nowplaying.py │ ├── playbackstamps.py │ ├── pmcolor.py │ ├── requiresasl.py │ ├── screensaver.py │ ├── serverpass.py │ ├── smartparts.py │ ├── statuscolor.py │ ├── twitch.py │ ├── undo.py │ ├── wordhl.py │ └── zncbuffer.py ├── ecdsa.py ├── growl.py ├── highlight.lua ├── itunes.py ├── lastfm.py ├── logpm.lua ├── masshighlightignore.lua ├── mymsg.lua ├── nowplaying.lua ├── playback.lua ├── playbackstamps.js ├── pmcolor.lua ├── pytabcomplete.py ├── readme.md ├── requiresasl.lua ├── screensaver.lua ├── screenshot.txt ├── script.py ├── serverpass.lua ├── session.py ├── smartparts.lua ├── statuscolor.lua ├── twitch.lua ├── undo.lua ├── undoclose.py ├── wordhl.lua └── zncbuffer.lua ├── Limnoria └── Markov │ ├── README.txt │ ├── __init__.py │ ├── config.py │ ├── plugin.py │ └── test.py ├── Textual ├── oban.py ├── readme.md └── ver.py ├── XChat ├── README.md ├── audacious.py ├── define.py ├── growl.py └── pithos.py └── license /.gitattributes: -------------------------------------------------------------------------------- 1 | text eol=lf 2 | -------------------------------------------------------------------------------- /Exaile/growl/PLUGININFO: -------------------------------------------------------------------------------- 1 | Version='0.0.1' 2 | Authors=['TingPing '] 3 | Name=_('Growl') 4 | Description=_('Growl notifications. Requires GNTP Module: https://github.com/kfdm/gntp') 5 | -------------------------------------------------------------------------------- /Exaile/growl/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from xl import event, player, common 3 | logger = logging.getLogger(__name__) 4 | try: 5 | import gntp.notifier 6 | except ImportError: 7 | logger.warning('gntp module not found; install https://github.com/kfdm/gntp') 8 | 9 | class ExaileGrowl(object): 10 | 11 | def __init__(self): 12 | self.exailelogo = 'http://upload.wikimedia.org/wikipedia/commons/7/70/Exaile-logo.png' 13 | self.growl = gntp.notifier.GrowlNotifier( 14 | applicationName='Exaile', 15 | notifications=['Song Changed'], 16 | defaultNotifications=['Song Changed'], 17 | applicationIcon=self.exailelogo, 18 | #hostname='localhost', 19 | #password='' 20 | ) 21 | self.growl.register() 22 | 23 | @common.threaded 24 | def on_play(self, type, player, track): 25 | self.growl.notify( 26 | noteType='Song Changed', 27 | title=track.get_tag_display('title'), 28 | description='by %s on %s' %(track.get_tag_display('artist'), track.get_tag_display('album')), 29 | icon=self.exailelogo, 30 | sticky=False, 31 | priority=0 32 | ) 33 | 34 | EXAILE_GROWL = ExaileGrowl() 35 | 36 | def enable(exaile): 37 | EXAILE_GROWL.exaile = exaile 38 | event.add_callback(EXAILE_GROWL.on_play, 'playback_track_start', player.PLAYER) 39 | 40 | 41 | def disable(exaile): 42 | event.remove_callback(EXAILE_GROWL.on_play, 'playback_track_start', player.PLAYER) 43 | 44 | -------------------------------------------------------------------------------- /HexChat/banhelper.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('BanHelper', '1', 'Simpllifies banning and quieting') 3 | 4 | local function is_op () 5 | local nick = hexchat.get_info('nick') 6 | for user in hexchat.iterate('users') do 7 | if hexchat.nickcmp(user.nick, nick) == 0 then 8 | return user.prefix:find('^[@&]') 9 | end 10 | end 11 | 12 | print('BanHelper: Warning, could not find self in userlist!') 13 | return false 14 | end 15 | 16 | local function get_mask (nick) 17 | if nick:find('[%*?!@%$]') then 18 | return nick -- Already a mask 19 | end 20 | 21 | for user in hexchat.iterate('users') do 22 | if hexchat.nickcmp(user.nick, nick) == 0 then 23 | if user.account then 24 | return '$a:' .. user.account 25 | elseif user.host then 26 | return '*!*@' .. user.host:match('@(.+)$') 27 | else 28 | print('BanHelper: Warning, user info not found, try enabling irc_who_join') 29 | return nil 30 | end 31 | end 32 | end 33 | end 34 | 35 | for _, command in pairs({'kickban', 'ban', 'quiet'}) do 36 | hexchat.hook_command(command, function (word, word_eol) 37 | if not word[2] then 38 | return hexchat.EAT_NONE 39 | end 40 | 41 | local caller = word[1]:lower() 42 | local channel = hexchat.get_info('channel') 43 | local mask = get_mask(word[2]) 44 | local command 45 | 46 | if not mask then 47 | return hexchat.EAT_ALL 48 | end 49 | 50 | if caller == 'ban' then 51 | command = 'MODE +b ' .. mask 52 | elseif caller == 'kickban' then 53 | command = string.format('MODE +b %s\r\nKICK %s %s :%s', mask, 54 | channel, word[2], word_eol[3] or '') 55 | elseif caller == 'quiet' then 56 | command = 'MODE +q ' .. mask 57 | end 58 | 59 | local wasop = is_op() 60 | if not wasop then 61 | hexchat.command('cs op ' .. channel) 62 | end 63 | hexchat.hook_timer(100, function () 64 | if is_op() then 65 | hexchat.command(command) 66 | if not wasop then 67 | hexchat.command('cs deop ' .. channel) 68 | end 69 | return false 70 | end 71 | 72 | return true 73 | end) 74 | 75 | return hexchat.EAT_HEXCHAT 76 | end) 77 | end 78 | -------------------------------------------------------------------------------- /HexChat/bansearch.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | import re 3 | 4 | __module_name__ = 'BanSearch' 5 | __module_author__ = 'TingPing' 6 | __module_version__ = '2' 7 | __module_description__ = 'Search for bans/quiets matching a user' 8 | 9 | banhook = 0 10 | quiethook = 0 11 | endbanhook = 0 12 | endquiethook = 0 13 | 14 | banlist = [] 15 | quietlist = [] 16 | 17 | regexescapes = {'[':r'\[', ']':r'\]', '.':r'\.'} 18 | ircreplace = {'{':'[', '}':']', '|':'\\'} # IRC nick substitutions 19 | wildcards = {'?':r'.', '*': r'.*'} # translate wildcards to regex 20 | 21 | def print_result(mask, matchlist, _type): 22 | if matchlist: 23 | print('\00318{}\017 had \00320{}\017 {} matches:'.format(mask, len(matchlist), _type)) 24 | for match in matchlist: 25 | print('\t\t\t{}'.format(match)) 26 | else: 27 | print('No {} matches for \00318{}\017 were found.'.format(_type, mask)) 28 | 29 | def match_mask(mask, searchmask): 30 | if searchmask is None: 31 | searchmask = '' 32 | 33 | # A mask of $a:* can match a user with no account 34 | if searchmask == '' and mask != '*': 35 | return False 36 | # A mask of $a will not match a user with no account 37 | elif mask == '' and searchmask != '': 38 | return True 39 | 40 | # These have to be replaced in a very specific order 41 | for match, repl in ircreplace.items(): 42 | mask = mask.replace(match, repl) 43 | searchmask = searchmask.replace(match, repl) 44 | for match, repl in regexescapes.items(): 45 | mask = mask.replace(match, repl) 46 | for match, repl in wildcards.items(): 47 | mask = mask.replace(match, repl) 48 | 49 | if '$' in mask and mask[0] != '$': # $#channel is used to forward users, ignore it 50 | mask = mask.split('$')[0] 51 | 52 | return bool(re.match(mask, searchmask, re.IGNORECASE)) 53 | 54 | def match_extban(mask, host, account, realname, usermask): 55 | try: 56 | extban, banmask = mask.split(':') 57 | except ValueError: 58 | extban = mask 59 | banmask = '' 60 | 61 | if '~' in extban: 62 | invert = True 63 | else: 64 | invert = False 65 | 66 | # Extbans from http://freenode.net/using_the_network.shtml 67 | if ':' in usermask: # Searching for extban 68 | userextban, usermask = usermask.split(':') 69 | if extban == userextban: 70 | ret = match_mask(banmask, usermask) 71 | else: 72 | return False 73 | elif 'a' in extban: 74 | ret = match_mask (banmask, account) 75 | elif 'r' in extban: 76 | ret = match_mask (banmask, realname) 77 | elif 'x' in extban: 78 | ret = match_mask (banmask, '{}#{}'.format(host, realname)) 79 | else: 80 | return False 81 | 82 | if invert: 83 | return not ret 84 | else: 85 | return ret 86 | 87 | def get_user_info(nick): 88 | invalid_chars = ['*', '?', '$', '@', '!'] 89 | if any(char in nick for char in invalid_chars): 90 | return (None, None, None) # It's a mask not a nick. 91 | 92 | for user in hexchat.get_list('users'): 93 | if user.nick == nick: 94 | host = user.nick + '!' + user.host 95 | account = user.account 96 | realname = user.realname 97 | return (host, account, realname) 98 | 99 | return (nick + '!*@*', None, None) 100 | 101 | def search_list(list, usermask): 102 | matchlist = [] 103 | host, account, realname = get_user_info (usermask) 104 | 105 | for mask in list: 106 | # If extban we require userinfo or we are searching for extban 107 | if mask[0] == '$' and (host or usermask[0] == '$'): 108 | if match_extban (mask, host, account, realname, usermask): 109 | matchlist.append(mask) 110 | elif mask[0] != '$': 111 | if host: # Was given a user 112 | if match_mask (mask, host): 113 | matchlist.append(mask) 114 | else: # Was given a mask or no userinfo found 115 | if match_mask (mask, usermask): 116 | matchlist.append(mask) 117 | 118 | return matchlist 119 | 120 | 121 | def banlist_cb(word, word_eol, userdata): 122 | global banlist 123 | banlist.append(word[4]) 124 | return hexchat.EAT_HEXCHAT 125 | 126 | def endbanlist_cb(word, word_eol, usermask): 127 | global banhook 128 | global endbanhook 129 | global banlist 130 | matchlist = [] 131 | 132 | hexchat.unhook(banhook) 133 | banhook = 0 134 | hexchat.unhook(endbanhook) 135 | endbanhook = 0 136 | 137 | if banlist: 138 | matchlist = search_list(banlist, usermask) 139 | banlist = [] 140 | 141 | print_result (usermask, matchlist, 'Ban') 142 | 143 | return hexchat.EAT_HEXCHAT 144 | 145 | def quietlist_cb(word, word_eol, userdata): 146 | global quietlist 147 | quietlist.append(word[5]) 148 | return hexchat.EAT_HEXCHAT 149 | 150 | def endquietlist_cb(word, word_eol, usermask): 151 | global quiethook 152 | global endquiethook 153 | global quietlist 154 | matchlist = [] 155 | 156 | hexchat.unhook(quiethook) 157 | quiethook = 0 158 | hexchat.unhook(endquiethook) 159 | endquiethook = 0 160 | 161 | if quietlist: 162 | matchlist = search_list(quietlist, usermask) 163 | quietlist = [] 164 | 165 | print_result (usermask, matchlist, 'Quiet') 166 | 167 | return hexchat.EAT_HEXCHAT 168 | 169 | def search_cb(word, word_eol, userdata): 170 | global banhook 171 | global quiethook 172 | global endbanhook 173 | global endquiethook 174 | 175 | if len(word) == 2: 176 | hooks = (quiethook, banhook, endquiethook, endbanhook) 177 | if not any(hooks): 178 | banhook = hexchat.hook_server ('367', banlist_cb) 179 | quiethook = hexchat.hook_server ('728', quietlist_cb) 180 | endbanhook = hexchat.hook_server ('368', endbanlist_cb, word[1]) 181 | endquiethook = hexchat.hook_server ('729', endquietlist_cb, word[1]) 182 | 183 | hexchat.command('ban') 184 | hexchat.command('quiet') 185 | else: 186 | print('A ban search is already in progress.') 187 | 188 | else: 189 | hexchat.command('help bansearch') 190 | 191 | return hexchat.EAT_ALL 192 | 193 | def unload_cb(userdata): 194 | print(__module_name__ + ' version ' + __module_version__ + ' unloaded.') 195 | 196 | hexchat.hook_unload(unload_cb) 197 | hexchat.hook_command('bansearch', search_cb, help='BANSEARCH ') 198 | hexchat.prnt(__module_name__ + ' version ' + __module_version__ + ' loaded.') 199 | -------------------------------------------------------------------------------- /HexChat/bookmarks.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = "Bookmarks" 4 | __module_author__ = "TingPing" 5 | __module_version__ = "2" 6 | __module_description__ = "Bookmark channels to easily rejoin them." 7 | 8 | def get_networks(name): 9 | pref = hexchat.get_pluginpref('bookmark_' + name) 10 | if not pref: 11 | return [] 12 | else: 13 | return pref.split(',') 14 | 15 | def load_bookmarks(): 16 | hexchat.command('menu -p-2 add "$TAB/Bookmark" "bookmark %s"') 17 | hexchat.command('menu add "$CHAN/Bookmark Channel" "bookmark %s"') 18 | hexchat.command('menu -p-3 add "Bookmarks"') 19 | hexchat.command('menu add "Bookmarks/-"') 20 | hexchat.command('menu add "Bookmarks/Add or Remove Current Channel" "bookmark"') 21 | for pref in hexchat.list_pluginpref(): 22 | if pref[:9] == 'bookmark_': 23 | chan = pref[9:] 24 | for net in get_networks(chan): 25 | hexchat.command('menu -p-2 add "Bookmarks/{}'.format(net)) 26 | hexchat.command('menu add "Bookmarks/{0}/{1}" "netjoin {1} {0}"'.format(net, chan)) 27 | 28 | def toggle_bookmark(chan, net): # It's a toggle because /menu sucks 29 | if chan == None: 30 | chan = hexchat.get_info('channel') 31 | 32 | if chan == '': 33 | return 34 | 35 | if net == None: 36 | try: # If from a $TAB it may be a different network. 37 | net = hexchat.find_context(None, chan).get_info('network') 38 | except AttributeError: 39 | net = hexchat.get_info('network') 40 | 41 | for channel in hexchat.get_list('channels'): 42 | if channel.channel == chan: 43 | if channel.type != 2: # Only bookmark channels 44 | return 45 | 46 | networks = get_networks(chan) 47 | pref = 'bookmark_' + chan 48 | if net in networks: # Remove 49 | if len(networks) == 1: 50 | hexchat.del_pluginpref(pref) 51 | else: 52 | networks.remove(net) 53 | hexchat.set_pluginpref(pref, ','.join(networks)) 54 | hexchat.command('menu del "Bookmarks/{}/{}"'.format(net, chan)) 55 | else: # Add 56 | networks.append(net) 57 | hexchat.set_pluginpref(pref, ','.join(networks)) 58 | hexchat.command('menu -p-2 add "Bookmarks/{}'.format(net)) 59 | hexchat.command('menu add "Bookmarks/{0}/{1}" "netjoin {1} {0}"'.format(net, chan)) 60 | 61 | def netjoin_cb(word, word_eol, userdata): 62 | joinchan = word[1] 63 | joinnet = word_eol[2] 64 | 65 | for chan in hexchat.get_list('channels'): 66 | if chan.network == joinnet: 67 | chan.context.command('join {}'.format(joinchan)) 68 | return hexchat.EAT_ALL 69 | 70 | # Not found, connect to network automatically. 71 | hexchat.command('url irc://"{}"/{}'.format(joinnet, joinchan)) 72 | return hexchat.EAT_ALL 73 | 74 | def bookmark_cb(word, word_eol, userdata): 75 | chan = None 76 | net = None 77 | 78 | try: 79 | chan = word[1] 80 | net = word[2] 81 | except IndexError: 82 | pass 83 | 84 | toggle_bookmark(chan, net) 85 | 86 | return hexchat.EAT_ALL 87 | 88 | def unload_callback(userdata): 89 | hexchat.command('menu del "Bookmarks"') 90 | hexchat.command('menu del "$TAB/Bookmark"') 91 | hexchat.command('menu del "$CHAN/Bookmark Channel"') 92 | print(__module_name__ + ' version ' + __module_version__ + ' unloaded.') 93 | 94 | hexchat.hook_command("bookmark", bookmark_cb, help='Usage: bookmark [channel] [network]\n\tToggles a bookmark.') 95 | hexchat.hook_command("netjoin", netjoin_cb, help='netjoin ') 96 | hexchat.hook_unload(unload_callback) 97 | load_bookmarks() 98 | print(__module_name__ + ' version ' + __module_version__ + ' loaded.') 99 | 100 | -------------------------------------------------------------------------------- /HexChat/buffextras.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('Buffextras', '1', "Format messages from ZNC's buffextras module") 3 | 4 | local function strip_brackets (str) 5 | return str:sub(2, #str - 1) 6 | end 7 | 8 | hexchat.hook_server_attrs('PRIVMSG', function (word, word_eol, attrs) 9 | if not word[1]:match('^:%*buffextras!') then 10 | return 11 | end 12 | 13 | local channel = word[3] 14 | local nick, host = word[4]:match('^:([^!]+)!(.*)$') 15 | 16 | -- don't eat modes & kicks from servers (we can't split them into nick + host) 17 | if nick == '' or nick == nil then 18 | nick = word[4]:sub(2) -- chop off leading ":" 19 | end 20 | 21 | local function is_event (event) 22 | return word_eol[5]:sub(1, #event) == event 23 | end 24 | 25 | local function emit (event, ...) 26 | hexchat.emit_print_attrs(attrs, event, ...) 27 | end 28 | 29 | if is_event('joined') then 30 | emit('Join', nick, channel, host) 31 | elseif is_event('quit with message') then 32 | emit('Quit', nick, strip_brackets(word_eol[8]), host) 33 | elseif is_event('quit:') then 34 | emit('Quit', nick, word_eol[6], host) 35 | elseif is_event('parted with message') then 36 | local reason = strip_brackets(word_eol[8]) 37 | if reason ~= '' then 38 | emit('Part with Reason', nick, host, channel, reason) 39 | else 40 | emit('Part', nick, host, channel) 41 | end 42 | elseif is_event('parted:') then 43 | local reason = word_eol[6] 44 | if reason ~= '' then 45 | emit('Part with Reason', nick, host, channel, reason) 46 | else 47 | emit('Part', nick, host, channel) 48 | end 49 | elseif is_event('is now known as') then 50 | emit('Change Nick', nick, word[9]) 51 | elseif is_event('changed the topic to') then 52 | emit('Topic Change', nick, word_eol[9], channel) 53 | elseif is_event('kicked') then 54 | if word[7] == "Reason:" then 55 | emit('Kick', nick, word[6], channel, strip_brackets(word_eol[8])) 56 | else 57 | emit('Kick', nick, word[6], channel, word_eol[9]) 58 | end 59 | elseif is_event('set mode') then 60 | emit('Raw Modes', nick, string.format('%s %s', channel, word_eol[7])) 61 | else 62 | return -- Unknown event 63 | end 64 | 65 | return hexchat.EAT_ALL 66 | end, hexchat.PRI_HIGH) 67 | -------------------------------------------------------------------------------- /HexChat/closeondisc.py: -------------------------------------------------------------------------------- 1 | import xchat as hexchat 2 | 3 | __module_name__ = 'closeondisc' 4 | __module_author__ = 'TingPing' 5 | __module_version__ = '2' 6 | __module_description__ = 'Close tabs on Disconnect' 7 | 8 | def discon_cb(word, word_eol, userdata): 9 | network = hexchat.get_info('network') 10 | 11 | for chan in hexchat.get_list('channels'): 12 | if chan.network == network and chan.type == 2: # Don't close server tab or queries 13 | chan.context.command('timer 1 close') # Delay to avoid crash 14 | 15 | hexchat.hook_print('Disconnected', discon_cb) 16 | -------------------------------------------------------------------------------- /HexChat/duplicates/banhelper.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = "Banhelper" 4 | __module_author__ = "TingPing" 5 | __module_version__ = "5" 6 | __module_description__ = "Simplifies banning and quieting" 7 | 8 | wasop = False 9 | 10 | def is_op(): 11 | my_nick = hexchat.get_info('nick') 12 | for user in hexchat.get_list('users'): 13 | if user.nick == my_nick: 14 | # FIXME: This might not work on all networks with 50 op levels.. 15 | if '@' in user.prefix or '&' in user.prefix: 16 | return True 17 | else: 18 | return False 19 | 20 | def do_op(deop=False): 21 | global wasop 22 | chan = hexchat.get_info('channel') 23 | 24 | if not deop: 25 | if not is_op(): 26 | wasop = False 27 | hexchat.command('cs op {}'.format(chan)) 28 | else: 29 | wasop = True 30 | else: 31 | if not wasop: 32 | hexchat.command('cs deop {}'.format(chan)) 33 | 34 | def do_command(cmd): 35 | if is_op(): 36 | hexchat.command(cmd) 37 | do_op(deop=True) 38 | return False 39 | 40 | return True 41 | 42 | def get_mask(nick): 43 | invalid_chars = ('*', '?', '!', '@', '$') 44 | if any(char in nick for char in invalid_chars): 45 | return nick # It's already a mask. 46 | 47 | for user in hexchat.get_list('users'): 48 | if hexchat.nickcmp(user.nick, nick) == 0: 49 | if user.account: 50 | return '$a:{}'.format(user.account) 51 | elif user.host: 52 | host = user.host.split('@')[1] 53 | return '*!*@{}'.format(host) 54 | else: 55 | hexchat.command('whois {}'.format(nick)) 56 | print('BH: User info not found, enable irc_who_join or try again.') 57 | return None 58 | 59 | def ban_cb(word, word_eol, userdata): 60 | if len(word) > 1: 61 | mask = get_mask(word[1]) 62 | 63 | if mask: 64 | do_op() 65 | 66 | if word[0] == 'ban': 67 | command = 'mode +b {}'.format(mask) 68 | elif word[0] == 'kickban': 69 | nick = word[1] 70 | chan = hexchat.get_info('channel') 71 | message = word_eol[2] if len(word_eol) > 2 else "" 72 | 73 | command = 'mode +b {}\r\nKICK {} {} :{}'.format(mask, chan, nick, message) 74 | elif word[0] == 'quiet': 75 | command = 'mode +q {}'.format(mask) 76 | 77 | hexchat.hook_timer(100, do_command, command) 78 | 79 | return hexchat.EAT_HEXCHAT 80 | else: 81 | return hexchat.EAT_NONE 82 | 83 | def unload_cb(userdata): 84 | print(__module_name__ + ' version ' + __module_version__ + ' unloaded.') 85 | 86 | hexchat.hook_command('kickban', ban_cb) 87 | hexchat.hook_command('ban', ban_cb) 88 | hexchat.hook_command('quiet', ban_cb) 89 | hexchat.hook_unload(unload_cb) 90 | print(__module_name__ + ' version ' + __module_version__ + ' loaded.') 91 | -------------------------------------------------------------------------------- /HexChat/duplicates/highlight.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = 'Highlight Logger' 4 | __module_version__ = '2' 5 | __module_description__ = 'Prints highlights to another tab' 6 | # Forked from Arnavion's 7 | 8 | TAB_NAME = '(highlights)' 9 | 10 | def find_highlighttab(): 11 | context = hexchat.find_context(channel=TAB_NAME) 12 | if context == None: # Create a new one in the background 13 | newtofront = hexchat.get_prefs('gui_tab_newtofront') 14 | 15 | hexchat.command('set -quiet gui_tab_newtofront 0') 16 | hexchat.command('newserver -noconnect {0}'.format(TAB_NAME)) 17 | hexchat.command('set -quiet gui_tab_newtofront {}'.format(newtofront)) 18 | 19 | return hexchat.find_context(channel=TAB_NAME) 20 | else: 21 | return context 22 | 23 | def highlight_callback(word, word_eol, user_data): 24 | 25 | word = [(word[i] if len(word) > i else '') for i in range(4)] 26 | 27 | highlight_context = find_highlighttab() 28 | 29 | channel = hexchat.get_info('channel') 30 | 31 | if user_data == 'Channel Msg Hilight': 32 | highlight_context.prnt('\00322{0}\t\00318<{4}{3}{1}>\017 {2}'.format(channel, *word)) 33 | elif user_data == 'Channel Action Hilight': 34 | highlight_context.prnt('\00322{0}\t\002\00318{4}{3}{1}\017 {2}'.format(channel, *word)) 35 | 36 | # Can't easily intelligently manage color so lets just ignore it 37 | highlight_context.command('gui color 0') 38 | 39 | return hexchat.EAT_NONE 40 | 41 | hexchat.hook_print('Channel Msg Hilight', highlight_callback, 'Channel Msg Hilight') 42 | hexchat.hook_print('Channel Action Hilight', highlight_callback, 'Channel Action Hilight') 43 | -------------------------------------------------------------------------------- /HexChat/duplicates/logpm.py: -------------------------------------------------------------------------------- 1 | import xchat as hexchat 2 | 3 | __module_name__ = "LogPMs" 4 | __module_author__ = "TingPing" 5 | __module_version__ = "2" 6 | __module_description__ = "Auto log pm's" 7 | 8 | def open_cb(word, word_eol, userdata): 9 | chan = hexchat.get_info('channel') 10 | # Assume nick if not prefixed with # 11 | # Ignore ZNC and Python 12 | # Use existing pref for nicks I usually ignore (i.e. chanserv) 13 | if chan and chan[0] != '#' \ 14 | and chan[0] != '*' \ 15 | and chan != '>>python<<' \ 16 | and chan not in hexchat.get_prefs('irc_no_hilight').split(','): 17 | hexchat.command('chanopt -quiet text_logging on') 18 | hexchat.command('chanopt -quiet text_scrollback on') 19 | 20 | hexchat.hook_print("Open Context", open_cb) 21 | -------------------------------------------------------------------------------- /HexChat/duplicates/mymsg.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = 'MyMessages' 4 | __module_author__ = 'TingPing' 5 | __module_version__ = '3' 6 | __module_description__ = 'Properly show your messages in ZNC' 7 | 8 | def privmsg_cb(word, word_eol, userdata, attrs): 9 | # We only care about private messages, HexChat handles the rest now. 10 | if word[2][0] == '#': 11 | return 12 | 13 | mynick = hexchat.get_info('nick') 14 | sender = word[0].split('!')[0][1:] 15 | recipient = word[2] 16 | network = hexchat.get_info('network') 17 | msg = word_eol[3][1:] 18 | 19 | if hexchat.nickcmp(sender, mynick) == 0 and hexchat.nickcmp(recipient, mynick) != 0: 20 | hexchat.command('query -nofocus {}'.format(recipient)) 21 | ctx = hexchat.find_context(network, recipient) 22 | 23 | if '\001ACTION' in msg: 24 | for repl in ('\001ACTION', '\001'): 25 | msg = msg.replace(repl, '') 26 | ctx.emit_print('Your Action', mynick, msg.strip(), time=attrs.time) 27 | else: 28 | ctx.emit_print('Your Message', mynick, msg, time=attrs.time) 29 | 30 | return hexchat.EAT_ALL 31 | 32 | def caps_cb(word, word_eol, userdata): 33 | if "znc.in/self-message" in word[1]: 34 | hexchat.command("cap req znc.in/self-message") 35 | 36 | hexchat.hook_server_attrs('PRIVMSG', privmsg_cb) 37 | hexchat.hook_print('Capability List', caps_cb) 38 | -------------------------------------------------------------------------------- /HexChat/duplicates/nowplaying.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import hexchat 3 | 4 | __module_name__ = 'NowPlaying' 5 | __module_author__ = 'TingPing' 6 | __module_version__ = '1' 7 | __module_description__ = 'Announce songs from mpris2 clients' 8 | 9 | try: 10 | import dbus 11 | from dbus.mainloop.glib import DBusGMainLoop 12 | import pympris 13 | print(__module_name__, 'version', __module_version__, 'loaded.') 14 | except ImportError: 15 | print('NP: Please install python-dbus and pympris.') 16 | hexchat.command('timer 0.1 py unload NowPlaying') 17 | 18 | np_help = 'NP [player]' 19 | 20 | session_bus = dbus.SessionBus(mainloop=DBusGMainLoop()) 21 | 22 | def print_nowplaying(mp, echo=False): 23 | metadata = mp.player.Metadata 24 | 25 | try: 26 | title = metadata['xesam:title'].encode('utf-8') 27 | artist = metadata['xesam:artist'][0].encode('utf-8') 28 | album = metadata['xesam:album'].encode('utf-8') 29 | except KeyError: 30 | print('NP: Song info not found.') 31 | return 32 | 33 | # TODO: Settings for these 34 | if echo: 35 | cmd = 'echo NP: {} by {} on {}.'.format(title, artist, album) 36 | elif hexchat.get_pluginpref('np_say'): 37 | cmd = 'say Now playing {} by {} on {}.'.format(title, artist, album) 38 | else: 39 | cmd = 'me is now playing {} by {} on {}.'.format(title, artist, album) 40 | 41 | hexchat.command(cmd) 42 | 43 | def np_cb(word, word_eol, userdata): 44 | player_ids = tuple(pympris.available_players()) 45 | player_num = len(player_ids) 46 | player_names = [pympris.MediaPlayer(_id).root.Identity for _id in player_ids] 47 | 48 | # TODO: commands for next/pause 49 | if player_num == 0: 50 | print('NP: No player found running.') 51 | elif player_num == 1: 52 | mp = pympris.MediaPlayer(player_ids[0], session_bus) 53 | print_nowplaying(mp) 54 | elif len(word) == 2: 55 | player = word[1] 56 | if player in player_names: 57 | index = player_names.index(player) 58 | mp = pympris.MediaPlayer(player_ids[index]) 59 | print_nowplaying(mp) 60 | else: 61 | print('NP: Player {} not found.'.format(player)) 62 | else: 63 | print('You have multple players running please insert a name:\n\t', ' '.join(player_names)) 64 | 65 | return hexchat.EAT_ALL 66 | 67 | def unload_cb(userdata): 68 | print(__module_name__, 'version', __module_version__, 'unloaded.') 69 | 70 | hexchat.hook_command('np', np_cb, help=np_help) 71 | hexchat.hook_unload(unload_cb) 72 | -------------------------------------------------------------------------------- /HexChat/duplicates/playbackstamps.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from datetime import date 3 | import hexchat 4 | 5 | __module_name__ = 'PlaybackStamps' 6 | __module_author__ = 'TingPing' 7 | __module_version__ = '1' 8 | __module_description__ = 'Prints date on older playback messages' 9 | 10 | edited = False 11 | 12 | events = ['Channel Message', 'Channel Msg Hilight', 13 | 'Channel Action', 'Channel Action Hilight', 14 | 'Your Action', 'Your Message'] 15 | 16 | def is_today(event_time): 17 | # 0 means now 18 | if not event_time: 19 | return True 20 | 21 | now = date.today() 22 | event = date.fromtimestamp(event_time) 23 | diff = now - event 24 | 25 | # Was over 24hrs ago 26 | if diff.total_seconds() > 86400: 27 | return False 28 | 29 | # Different date 30 | if event.year < now.year or event.month < now.month or event.day < now.day: 31 | return False 32 | 33 | return True 34 | 35 | def msg_cb(word, word_eol, event_name, attrs): 36 | global edited 37 | 38 | if edited or is_today(attrs.time): 39 | return 40 | 41 | format = hexchat.get_prefs('stamp_text_format') 42 | hexchat.command('set -quiet stamp_text_format {}'.format('%m-%d ' + format)) 43 | 44 | edited = True 45 | hexchat.emit_print(event_name, word[0], word[1], time=attrs.time) 46 | edited = False 47 | 48 | hexchat.command('set -quiet stamp_text_format {}'.format(format)) 49 | 50 | return hexchat.EAT_ALL 51 | 52 | def unload_cb(userdata): 53 | print(__module_name__, 'version', __module_version__, 'unloaded.') 54 | 55 | for event in events: 56 | hexchat.hook_print_attrs(event, msg_cb, event) 57 | hexchat.hook_unload(unload_cb) 58 | print(__module_name__, 'version', __module_version__, 'loaded.') 59 | -------------------------------------------------------------------------------- /HexChat/duplicates/pmcolor.py: -------------------------------------------------------------------------------- 1 | __module_name__ = "PMColor" 2 | __module_author__ = "TingPing" 3 | __module_version__ = "1" 4 | __module_description__ = "Color PM tabs like Hilights" 5 | 6 | import xchat 7 | 8 | def pm_cb(word, word_eol, userdata): 9 | xchat.command('GUI COLOR 3') 10 | return None 11 | 12 | xchat.hook_print("Private Message to Dialog", pm_cb) 13 | xchat.hook_print("Private Action to Dialog", pm_cb) 14 | -------------------------------------------------------------------------------- /HexChat/duplicates/requiresasl.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = 'RequireSASL' 4 | __module_author__ = 'TingPing' 5 | __module_version__ = '1' 6 | __module_description__ = 'Disconnect if SASL fails' 7 | 8 | def response_cb(word, word_eol, userdata): 9 | if word[1] == '904': # Failed 10 | hexchat.command('timer .1 discon') # Delay so disconnect happens after message. 11 | 12 | hexchat.hook_print('SASL Response', response_cb) 13 | -------------------------------------------------------------------------------- /HexChat/duplicates/screensaver.py: -------------------------------------------------------------------------------- 1 | # Originally written by Wil Cooley 2 | # http://haus.nakedape.cc/svn/public/trunk/small-projects/desktop/screensaverAutoAway.py 3 | 4 | import dbus 5 | from dbus.mainloop.glib import DBusGMainLoop 6 | DBusGMainLoop(set_as_default=True) 7 | 8 | import hexchat 9 | 10 | __module_author__= 'TingPing' 11 | __module_name__ = 'screensaver' 12 | __module_version__ = '2' 13 | __module_description__ = 'Sets user away when the GNOME screensaver is activated' 14 | 15 | screensavers = ('org.gnome.ScreenSaver', 16 | 'org.cinnamon.ScreenSaver', 17 | 'org.freedesktop.ScreenSaver') 18 | 19 | sesbus = dbus.SessionBus() 20 | 21 | def screensaver_cb(state): 22 | if state: 23 | hexchat.command('allserv away') 24 | else: 25 | hexchat.command('allserv back') 26 | 27 | def unload_cb(userdata): 28 | print(__module_name__ + ' version ' + __module_version__ + ' unloaded.') 29 | 30 | 31 | for screensaver in screensavers: 32 | sesbus.add_signal_receiver(screensaver_cb, 'ActiveChanged', screensaver) 33 | 34 | hexchat.hook_unload(unload_cb) 35 | print(__module_name__ + ' version ' + __module_version__ + ' loaded.') 36 | -------------------------------------------------------------------------------- /HexChat/duplicates/serverpass.py: -------------------------------------------------------------------------------- 1 | import xchat as hexchat 2 | 3 | __module_name__ = 'ServerPass' 4 | __module_author__ = 'TingPing' 5 | __module_version__ = '1' 6 | __module_description__ = 'Alows using server password with another login method' 7 | # This works around 2.9.6+'s new login methods 8 | 9 | networks = ('freenode', ) 10 | 11 | def connected_cb(word, word_eol, userdata): 12 | if hexchat.get_info('network') in networks: 13 | username = hexchat.get_prefs ('irc_user_name') 14 | password = hexchat.get_info('password') 15 | hexchat.command('quote PASS {}:{}'.format(username, password)) 16 | 17 | hexchat.hook_print('Connected', connected_cb) 18 | -------------------------------------------------------------------------------- /HexChat/duplicates/smartparts.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | import hexchat 3 | 4 | __module_name__ = 'SmartParts' 5 | __module_author__ = 'TingPing' 6 | __module_version__ = '2' 7 | __module_description__ = 'Intelligently hide parts, joins, autoop, and nick changes' 8 | 9 | def check_notify (nick): 10 | for user in hexchat.get_list('notify'): 11 | if user.nick == hexchat.strip(nick): 12 | return True 13 | 14 | return False 15 | 16 | def check_lasttalk (nick): 17 | for user in hexchat.get_list('users'): 18 | if user.nick == hexchat.strip(nick): 19 | if time() - user.lasttalk > 60 * 5: 20 | return hexchat.EAT_HEXCHAT 21 | else: 22 | return hexchat.EAT_NONE 23 | 24 | return hexchat.EAT_HEXCHAT 25 | 26 | def check_you (nick): 27 | return not hexchat.nickcmp(hexchat.get_info('nick'), nick) 28 | 29 | def nick_cb(word, word_eol, userdata): 30 | if check_notify(word[0]) or check_notify(word[1]): 31 | return hexchat.EAT_NONE 32 | 33 | return check_lasttalk(word[0]) 34 | 35 | def mode_cb(word, word_eol, userdata): 36 | if check_you(word[0]) or check_you(word[1]): 37 | return hexchat.EAT_NONE 38 | 39 | if check_notify(word[1]): 40 | return hexchat.EAT_NONE 41 | 42 | return check_lasttalk(word[1]) 43 | 44 | def raw_mode_cb(word, word_eol, userdata): 45 | if check_you(word[0]) or check_notify(word[0]): 46 | return hexchat.EAT_NONE 47 | 48 | # TODO: Parse targets and check them 49 | 50 | return check_lasttalk(word[0]) 51 | 52 | def join_cb(word, word_eol, userdata): 53 | if check_notify(word[0]): 54 | return hexchat.EAT_NONE 55 | else: 56 | return hexchat.EAT_HEXCHAT 57 | 58 | def part_cb(word, word_eol, userdata): 59 | if check_notify(word[0]): 60 | return hexchat.EAT_NONE 61 | 62 | return check_lasttalk(word[0]) 63 | 64 | for event in ('Quit', 'Part', 'Part with Reason'): 65 | hexchat.hook_print(event, part_cb) 66 | for event in ('Channel Operator', 'Channel Voice'): 67 | hexchat.hook_print(event, mode_cb) 68 | hexchat.hook_print('Raw Modes', raw_mode_cb) 69 | hexchat.hook_print('Join', join_cb) 70 | hexchat.hook_print('Change Nick', nick_cb) 71 | -------------------------------------------------------------------------------- /HexChat/duplicates/statuscolor.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = 'statuscolor' 4 | __module_author__ = 'TingPing' 5 | __module_version__ = '0' 6 | __module_description__ = 'Colors nicks based on user mode' 7 | # This is just a direct port of LifeIsPain's statuscolor.pl script. 8 | 9 | events = ('Channel Message', 10 | 'Channel Action', 11 | 'Channel Msg Hilight', 12 | 'Channel Action Hilight', 13 | 'Your Message', 14 | 'Your Action') 15 | 16 | # TODO: update these for hexchats default theme 17 | modes = {'+':'24', 18 | '%':'28', 19 | '@':'19', 20 | '&':'21', 21 | '~':'22'} 22 | 23 | edited = False 24 | 25 | def msg_cb(word, word_eol, event, attrs): 26 | global edited 27 | if edited or len(word) < 3 or not word[2] in modes: 28 | return hexchat.EAT_NONE 29 | 30 | color = modes[word[2]] 31 | nick = '\003{}{}\00399'.format(color, hexchat.strip(word[0])) 32 | word = [(word[i] if len(word) > i else '') for i in range(4)] 33 | 34 | edited = True 35 | hexchat.emit_print(event, nick, word[1], word[2], word[3], time=attrs.time) 36 | edited = False 37 | 38 | return hexchat.EAT_ALL 39 | 40 | for event in events: 41 | hexchat.hook_print_attrs(event, msg_cb, event, hexchat.PRI_HIGH) 42 | -------------------------------------------------------------------------------- /HexChat/duplicates/twitch.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = 'Twitch' 4 | __module_author__ = 'TingPing' 5 | __module_version__ = '4' 6 | __module_description__ = 'Better integration with Twitch.tv' 7 | # Very much a work in progress... 8 | 9 | # Commands from http://help.twitch.tv/customer/portal/articles/659095-chat-moderation-commands 10 | # /ban may conflict with other scripts nothing we can do about that 11 | # /clear is an existing command, just override it 12 | commands = ('timeout', 'slow', 'slowoff', 'subscribers', 'subscribersoff', 13 | 'mod', 'unmod', 'mods', 'clear', 'ban', 'unban', 'commercial', 14 | 'r9kbeta', 'r9kbetaoff', 'color', 'host') 15 | 16 | aliases = {'op':'mod', 'deop':'unmod'} 17 | 18 | def twitchOnly(func): 19 | def if_twitch(*args, **kwargs): 20 | for host in (hexchat.get_info('host'), hexchat.get_info('server')): 21 | if host and 'twitch.tv' in host: 22 | return func(*args, **kwargs) 23 | else: 24 | return hexchat.EAT_NONE 25 | return if_twitch 26 | 27 | # Twitch returns a lot of 'unknown command' errors, ignore them. 28 | @twitchOnly 29 | def servererr_cb(word, word_eol, userdata): 30 | if word[3] in ('WHO', 'WHOIS'): 31 | return hexchat.EAT_ALL 32 | 33 | #@twitchOnly 34 | #def endofmotd_cb(word, word_eol, userdata): 35 | # TODO: This shows the output of commands like /slow but also 36 | # tons of other garbage we have to ignore.. 37 | # hexchat.command('QUOTE TWITCHCLIENT 3') 38 | 39 | # Print jtv messages in server tab. 40 | @twitchOnly 41 | def privmsg_cb(word, word_eol, userdata): 42 | if word[0][1:].split('!')[0] == 'jtv': 43 | if word[2][0] != '#': 44 | for chan in hexchat.get_list('channels'): 45 | if chan.type == 1 and chan.id == hexchat.get_prefs('id'): 46 | chan.context.emit_print('Server Text', word_eol[3][1:]) 47 | return hexchat.EAT_ALL 48 | elif word[3] in (':USERCOLOR', ':USERSTATE', ':HISTORYEND', ':EMOTESET', ':CLEARCHAT', ':SPECIALUSER'): 49 | return hexchat.EAT_ALL 50 | 51 | # Eat any message starting with a '.', twitch eats all of them too. 52 | @twitchOnly 53 | def yourmsg_cb(word, word_eol, userdata): 54 | if word[1][0] == '.': 55 | return hexchat.EAT_ALL 56 | 57 | # Just prefix with a '.'. 58 | @twitchOnly 59 | def command_cb(word, word_eol, alias): 60 | if alias: 61 | if len(word_eol) > 1: 62 | hexchat.command('say .{} {}'.format(alias, word_eol[1])) 63 | else: 64 | hexchat.command('say .{}'.format(alias)) 65 | else: 66 | hexchat.command('say .{}'.format(word_eol[0])) 67 | return hexchat.EAT_ALL 68 | 69 | for command in commands: 70 | hexchat.hook_command(command, command_cb) 71 | for command, alias in aliases.items(): 72 | hexchat.hook_command(command, command_cb, alias) 73 | hexchat.hook_print('Your Message', yourmsg_cb) 74 | hexchat.hook_server('421', servererr_cb) 75 | #hexchat.hook_server('376', endofmotd_cb) 76 | hexchat.hook_server('PRIVMSG', privmsg_cb) 77 | -------------------------------------------------------------------------------- /HexChat/duplicates/undo.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from collections import deque 3 | from sys import platform 4 | import xchat as hexchat 5 | 6 | __module_name__ = "Undo" 7 | __module_author__ = "TingPing" 8 | __module_version__ = "1" 9 | __module_description__ = "Binds ctrl+z to undo and ctrl+y/shift+ctrl+z to redo." 10 | 11 | undolevels = 10 12 | redolevels = 10 13 | 14 | undobufs = {} 15 | redobufs = {} 16 | 17 | if platform == 'darwin': 18 | primarymod = 1 << 28 19 | else: 20 | primarymod = 1 << 2 21 | shiftmod = 1 << 0 22 | 23 | def get_valid_mod (mod): 24 | """Modifiers are full of junk we dont care about, remove them""" 25 | return int(mod) & (1 << 0 | 1 << 2 | 1 << 3 | 1 << 28) 26 | 27 | def keypress_cb(word, word_eol, userdata): 28 | global undobufs 29 | global redobufs 30 | bufname = '{}_{}'.format(hexchat.get_info('channel'), hexchat.get_info('network')) 31 | key = word[0] 32 | mod = get_valid_mod(word[1]) 33 | inputtext = hexchat.get_info('inputbox') 34 | 35 | # Previous strings are stored as deque's in a dict for each channel 36 | if not bufname in undobufs: 37 | undobuflist = undobufs[bufname] = deque(maxlen=undolevels) 38 | else: 39 | undobuflist = undobufs[bufname] 40 | 41 | if not bufname in redobufs: 42 | redobuflist = redobufs[bufname] = deque(maxlen=redolevels) 43 | else: 44 | redobuflist = redobufs[bufname] 45 | 46 | if (key, mod) == ('122', primarymod): # ctrl+z 47 | try: 48 | # Get last saved string 49 | text = undobuflist.pop() 50 | if text == inputtext: # First undo may result in same text 51 | redobuflist.append(text) 52 | text = undobuflist.pop() 53 | hexchat.command('settext {}'.format(text)) 54 | hexchat.command('setcursor {}'.format(len(text))) 55 | 56 | redobuflist.append(text) 57 | 58 | except IndexError: pass # No undos left 59 | 60 | elif ((key, mod) == ('121', primarymod) or # ctrl+y 61 | (key, mod) == ('90', shiftmod|primarymod)): # ctrl+shift+z 62 | try: 63 | text = redobuflist.pop() 64 | if text == inputtext: 65 | text = redobuflist.pop() 66 | hexchat.command('settext {}'.format(text)) 67 | hexchat.command('setcursor {}'.format(len(text))) 68 | 69 | except IndexError: pass 70 | 71 | else: 72 | # Just throw anything else in here if it has changed 73 | if not undobuflist or undobuflist[-1] != inputtext: 74 | undobuflist.append(inputtext) 75 | 76 | def unload_cb(userdata): 77 | print(__module_name__, 'version', __module_version__, 'unloaded.') 78 | 79 | hexchat.hook_print('Key Press', keypress_cb) 80 | hexchat.hook_unload(unload_cb) 81 | print(__module_name__, 'version', __module_version__, 'loaded.') 82 | -------------------------------------------------------------------------------- /HexChat/duplicates/wordhl.py: -------------------------------------------------------------------------------- 1 | import re 2 | import hexchat 3 | 4 | __module_name__ = 'wordhl' 5 | __module_author__ = 'TingPing' 6 | __module_version__ = '2' 7 | __module_description__ = 'Highlights some words of importance' 8 | # When you want to notice something, but not really get 'highlighted' 9 | 10 | # Case insensitive words 11 | hlwords = ('hexchat', ) 12 | 13 | # Don't touch 14 | edited = False 15 | 16 | def print_cb(word, word_eol, event, attr): 17 | global edited 18 | # Ignore our own events, bouncer playback, empty messages 19 | if edited or attr.time or not len(word) > 1: 20 | return 21 | 22 | msg = word[1] 23 | for _word in hlwords: 24 | # Color red 25 | msg = re.sub(_word, '\00320' + _word + '\00399', msg, flags=re.I) 26 | 27 | if msg != word[1]: # Something replaced 28 | edited = True 29 | word = [(word[i] if len(word) > i else '') for i in range(4)] 30 | hexchat.emit_print(event, word[0], msg, word[2], word[3]) 31 | edited = False 32 | 33 | hexchat.command('gui color 3') 34 | return hexchat.EAT_ALL 35 | 36 | hexchat.hook_print_attrs('Channel Message', print_cb, 'Channel Message', priority=hexchat.PRI_HIGH) 37 | hexchat.hook_print_attrs('Channel Action', print_cb, 'Channel Action', priority=hexchat.PRI_HIGH) 38 | -------------------------------------------------------------------------------- /HexChat/duplicates/zncbuffer.py: -------------------------------------------------------------------------------- 1 | import hexchat 2 | 3 | __module_name__ = 'ZNC Buffers' 4 | __module_author__ = 'fladd & TingPing' 5 | __module_version__ = '1.0' 6 | __module_description__ = 'Add menu options to manage ZNC buffers' 7 | 8 | # TODO: 9 | # Don't run on non-znc networks 10 | # Actually check for channel type 11 | 12 | recently_cleared = [] 13 | 14 | def privmsg_cb(word, word_eol, userdata): 15 | # ZNC helpfully tells us what we just did.. so lets hide that spam 16 | if word[0] == ':*status!znc@znc.in' and len(word_eol) > 4 and word_eol[4].startswith('buffers matching'): 17 | cleared = word[6][1:-1] # [nick] 18 | if cleared in recently_cleared: 19 | recently_cleared.remove(cleared) 20 | return hexchat.EAT_ALL 21 | 22 | def clearbuffer_cmd_cb(word, word_eol, userdata): 23 | # We just want to track which one is cleared by menu to silence it 24 | name = word[1] 25 | 26 | # Ignore ZNC queries and channels 27 | if name[0] != '*': 28 | recently_cleared.append(name) 29 | hexchat.command('znc clearbuffer {}'.format(name)) 30 | 31 | return hexchat.EAT_ALL 32 | 33 | def unload_cb(userdata): 34 | # Remove menu entries 35 | hexchat.command('menu del "$TAB/ZNC"') 36 | 37 | hexchat.command('menu -p4 add "$TAB/ZNC"') 38 | hexchat.command('menu add "$TAB/ZNC/Clear Buffer" ".zncclearbuffer %s"') 39 | hexchat.command('menu add "$TAB/ZNC/Play Buffer" "znc playbuffer %s"') 40 | hexchat.hook_unload(unload_cb) 41 | hexchat.hook_command('.zncclearbuffer', clearbuffer_cmd_cb) 42 | hexchat.hook_server('PRIVMSG', privmsg_cb) 43 | -------------------------------------------------------------------------------- /HexChat/ecdsa.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import base64 3 | import subprocess 4 | from os import path 5 | import hexchat 6 | 7 | __module_name__ = 'ECDSA' 8 | __module_author__ = 'TingPing' 9 | __module_version__ = '0' 10 | __module_description__ = 'Implements ECDSA SASL mech' 11 | 12 | 13 | # README 14 | # 15 | # This script is not necessarily complete or user friendly (this mech isn't in general) 16 | # but it does work here are some basic steps: 17 | # 18 | # - Install https://github.com/atheme/ecdsatool 19 | # - Run `ecdsatool keygen ~/.config/hexchat/irc.pem` 20 | # - Take the output of that and run `/msg nickserv set property pubkey THEKEY` 21 | # - Configure options below 22 | # 23 | # I recommend also using my requiresasl.py script for a more secure experience 24 | # 25 | 26 | # CONFIGUREME 27 | NETWORKS = ( 28 | 'freenode-testnet', 29 | ) 30 | USERNAME = '' # If empty uses global username 31 | KEY_FILE = path.join(hexchat.get_info('configdir'), 'irc.pem') 32 | 33 | 34 | def cap_cb(word, word_eol, userdata): 35 | if not hexchat.get_info('network') in NETWORKS: 36 | return 37 | 38 | # We will handle SASL for this one instead of hexchat 39 | if word[3] == 'ACK' and 'sasl' in word_eol[4]: 40 | hexchat.command('recv {}'.format(word_eol[0].replace('sasl', ''))) 41 | hexchat.command('quote AUTHENTICATE ECDSA-NIST256P-CHALLENGE') 42 | return hexchat.EAT_ALL 43 | 44 | def auth_cb(word, word_eol, userdata): 45 | if not hexchat.get_info('network') in NETWORKS: 46 | return 47 | 48 | # This will fail if user does not have pubkey set 49 | if word[1] == '+': 50 | user = hexchat.get_prefs('irc_user_name') if not USERNAME else USERNAME 51 | hexchat.emit_print('SASL Authenticating', user, 'ECDSA-NIST256P-CHALLENGE') 52 | encoded = base64.b64encode(user + '\000' + user) 53 | hexchat.command('quote AUTHENTICATE {}'.format(encoded)) 54 | return hexchat.EAT_ALL 55 | 56 | try: 57 | ret = subprocess.check_output(['ecdsatool', 'sign', KEY_FILE, word_eol[1]]) 58 | hexchat.command('quote AUTHENTICATE {}'.format(ret)) # TODO: Long messages? 59 | except (subprocess.CalledProcessError, OSError) as e: 60 | print('Error running ecdsatool: {}'.format(e)) 61 | hexchat.command('quote AUTHENTICATE *') 62 | 63 | return hexchat.EAT_ALL 64 | 65 | def unload_cb(userdata): 66 | print(__module_name__, 'version', __module_version__, 'unloaded.') 67 | 68 | hexchat.hook_server('CAP', cap_cb) 69 | hexchat.hook_server('AUTHENTICATE', auth_cb) 70 | hexchat.hook_unload(unload_cb) 71 | print(__module_name__, 'version', __module_version__, 'loaded.') 72 | -------------------------------------------------------------------------------- /HexChat/growl.py: -------------------------------------------------------------------------------- 1 | __module_name__='Growl' 2 | __module_description__='Growl notification support' 3 | __module_author__='TingPing' 4 | __module_version__='16' 5 | 6 | from time import time 7 | import xchat 8 | try: 9 | import gntp.notifier 10 | except ImportError: 11 | xchat.prnt('\002\00304Growl Error:\017 Please install \037https://github.com/kfdm/gntp\017') 12 | xchat.command('timer 0.1 py unload Growl') 13 | 14 | hexchatlogo = 'https://raw.github.com/hexchat/hexchat/master/data/icons/hexchat.png' 15 | lasttime = time() 16 | lastnick = '' 17 | lasterrtime = 0 18 | 19 | # initial setup of growl and list of possible notifications 20 | # hostname and password are for over the network notifications 21 | growl = gntp.notifier.GrowlNotifier( 22 | applicationName='HexChat', 23 | notifications=['Highlight', 'Private Message', 'Invited', 'Topic Changed', 24 | 'User Online', 'Server Notice', 'Disconnected', 'Banned', 25 | 'Killed', 'Kicked', 'Custom'], 26 | defaultNotifications=['Highlight', 'Private Message', 'Invited', 'Server Notice', 27 | 'Disconnected', 'Killed', 'Kicked', 'Banned', 'Custom'], 28 | applicationIcon=hexchatlogo, 29 | #hostname='localhost', 30 | #password='' 31 | ) 32 | 33 | try: 34 | growl.register() 35 | except: 36 | xchat.prnt('Growl Error: Could not register with Growl') 37 | 38 | 39 | def growlnotify(_type, title, desc='', pri=0): 40 | if xchat.get_prefs('away_omit_alerts') and xchat.get_info('away'): 41 | return 42 | 43 | if xchat.get_prefs('gui_focus_omitalerts') and xchat.get_info('win_status') == 'active': 44 | return 45 | 46 | try: 47 | growl.notify( 48 | noteType=_type, 49 | title=xchat.strip(title), 50 | description=xchat.strip(desc), 51 | icon=hexchatlogo, 52 | sticky=False, 53 | priority=pri 54 | ) 55 | except: 56 | global lasterrtime 57 | # Avoid more spam, 1 error a min. 58 | if lasterrtime + 60 < time(): 59 | xchat.prnt('Growl Error: Growl is not running.') 60 | lasterrtime = time() 61 | 62 | 63 | # now checks for and ignores mass hilights, performance impact not yet tested, maybe removed, optional, or only used on small channels 64 | # disabled for now 65 | # def masshilight(nick, message): 66 | # userlist = '' 67 | 68 | # for user in xchat.get_list('users'): 69 | # if user.nick != word[0]: 70 | # userlist += user.nick + ' ' 71 | 72 | # if re.search(userlist[:-1], xchat.strip(message)): 73 | # return True 74 | 75 | # else: 76 | # return False 77 | 78 | def spam(currenttime, currentnick): 79 | # Highlight and PM now have spam protection which previously could hang XChat 80 | global lasttime 81 | global lastnick 82 | 83 | if xchat.nickcmp(lastnick, currentnick) != 0: 84 | lasttime = time() 85 | lastnick = currentnick 86 | return False 87 | 88 | elif lasttime + 3 < currenttime: 89 | lasttime = time() 90 | return False 91 | 92 | else: 93 | lasttime = time() 94 | return True 95 | 96 | def active(chan): 97 | # Checks to see if chat is active to reduce annoying notifications 98 | try: 99 | chat = xchat.find_context() 100 | currentchat = chat.get_info("channel") 101 | status = xchat.get_info("win_status") 102 | if currentchat == chan and status == "active": 103 | return True 104 | else: 105 | return False 106 | except: 107 | return False 108 | 109 | 110 | # start list of notifications 111 | def hilight_callback(word, word_eol, userdata): 112 | if not spam(time(), word[0]): # and not masshilight(word[0], word[1]): 113 | growlnotify('Highlight', 114 | 'Highlight by ' + word[0], 115 | word[1], 116 | 1) 117 | 118 | def pm_callback(word, word_eol, userdata): 119 | if not spam(time(), word[0]) and not active(word[0]): 120 | growlnotify('Private Message', 121 | 'Messaged by ' + word[0], 122 | word[1], 123 | 1) 124 | 125 | def invited_callback(word, word_eol, userdata): 126 | growlnotify('Invited', 127 | 'Invited to ' + word[0], 128 | 'Invited to %s by %s on %s' % (word[0], word[1], word[2])) 129 | 130 | def topic_callback(word, word_eol, userdata): 131 | growlnotify('Topic Changed', 132 | word[2] + '\'s topic changed', 133 | '%s \'s topic changed to %s by %s' % (word[2], word[1], word[0]), 134 | -2) 135 | 136 | def onlinenotify_callback(word, word_eol, userdata): 137 | growlnotify('User Online', 138 | word[0] + ' is online on ' + word[2]) 139 | 140 | def servernotice_callback(word, word_eol, userdata): 141 | growlnotify('Server Notice', 142 | 'Notice from ' + word[1], 143 | word[0]) 144 | 145 | def disconnect_callback(word, word_eol, userdata): 146 | growlnotify('Disconnected', 147 | 'Disonnected from server', 148 | word[0], 149 | 1) 150 | 151 | def killed_callback(word, word_eol, userdata): 152 | growlnotify('Killed', 153 | 'Killed by ' + word[0], 154 | word[1], 155 | 2) 156 | 157 | def kicked_callback(word, word_eol, userdata): 158 | growlnotify('Kicked', 159 | 'You have been kicked from ' + word[2], 160 | 'Kicked by %s for %s' % (word[1], word[3]), 161 | 1) 162 | 163 | def banned_callback(word, word_eol, userdata): 164 | # this now works on a basic level, will possibly be improved 165 | nick = xchat.get_info('nick') 166 | for user in xchat.get_list('users'): 167 | if xchat.nickcmp(nick, user.nick) == 0: 168 | userhost = user.host 169 | if not userhost: 170 | return None 171 | hostip = userhost.split('@')[1] 172 | 173 | if nick in word[1] or hostip in word[1]: 174 | growlnotify('Banned', 175 | 'You have been banned by ' + word[0]) 176 | 177 | def tray_callback(word, word_eol, userdata): 178 | if len(word) > 3 and word[1] == '-b': 179 | growlnotify('Custom', word[2], word_eol[3], 1) 180 | return xchat.EAT_ALL 181 | 182 | def unload_callback(userdata): 183 | xchat.prnt(__module_name__ + ' version ' + __module_version__ + ' unloaded.') 184 | 185 | # get events from hexchat to call notifications 186 | xchat.hook_print("Channel Msg Hilight", hilight_callback) 187 | xchat.hook_print("Channel Action Hilight", hilight_callback) 188 | xchat.hook_print("Private Message to Dialog", pm_callback) 189 | xchat.hook_print("Private Action to Dialog", pm_callback) 190 | xchat.hook_print("Invited", invited_callback) 191 | xchat.hook_print("Notice", servernotice_callback) 192 | xchat.hook_print("Notify Online", onlinenotify_callback) 193 | xchat.hook_print("Topic Change", topic_callback) 194 | xchat.hook_print("You Kicked", kicked_callback) 195 | xchat.hook_print("Killed", killed_callback) 196 | xchat.hook_print("Channel Ban", banned_callback) 197 | # hook the tray command for other scripts to use 198 | xchat.hook_command("tray", tray_callback) 199 | # just to print its unloaded 200 | xchat.hook_unload(unload_callback) 201 | # Nothing broke yet, its loaded! =) 202 | xchat.prnt(__module_name__ + ' version ' + __module_version__ + ' loaded.') 203 | -------------------------------------------------------------------------------- /HexChat/highlight.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('Highlights', '1', 'Prints highlights to another tab') 3 | 4 | local TAB_NAME = '(highlights)' 5 | local OPEN_PER_SERVER = false 6 | 7 | local function find_highlighttab () 8 | local network = nil 9 | if OPEN_PER_SERVER then 10 | network = hexchat.get_info('network') 11 | end 12 | local ctx = hexchat.find_context(network, TAB_NAME) 13 | if not ctx then 14 | if OPEN_PER_SERVER then 15 | hexchat.command('query -nofocus ' .. TAB_NAME) 16 | else 17 | local newtofront = hexchat.prefs['gui_tab_newtofront'] 18 | hexchat.command('set -quiet gui_tab_newtofront off') 19 | hexchat.command('newserver -noconnect ' .. TAB_NAME) 20 | hexchat.command('set -quiet gui_tab_newtofront ' .. tostring(newtofront)) 21 | end 22 | 23 | return hexchat.find_context(network, TAB_NAME) 24 | end 25 | 26 | return ctx 27 | end 28 | 29 | local function on_highlight (args, event_type) 30 | local channel = hexchat.get_info('channel') 31 | local highlight_context = find_highlighttab() 32 | 33 | local format 34 | if event_type == 'Channel Msg Hilight' then 35 | format = '\00322%s\t\00318<%s%s%s>\015 %s' 36 | elseif event_type == 'Channel Action Hilight' then 37 | format = '\00322%s\t\002\00318%s%s%s\015 %s' 38 | end 39 | 40 | highlight_context:print(string.format(format, channel, 41 | args[3] or '', args[4] or '', hexchat.strip(args[1]), args[2])) 42 | highlight_context:command('gui color 0') -- Ignore colors 43 | end 44 | 45 | for _, event in ipairs({'Channel Msg Hilight', 'Channel Action Hilight'}) do 46 | hexchat.hook_print(event, function (args) 47 | return on_highlight(args, event) 48 | end, hexchat.PRI_LOW) 49 | end 50 | -------------------------------------------------------------------------------- /HexChat/itunes.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import hexchat 3 | 4 | __module_name__ = 'iTunes' 5 | __module_author__ = 'TingPing' 6 | __module_version__ = '0' 7 | __module_description__ = 'Announces current song playing in iTunes for Windows' 8 | 9 | loaded = False 10 | try: 11 | import win32com.client 12 | import pywintypes 13 | loaded = True 14 | print(__module_name__, 'version', __module_version__, 'loaded.') 15 | except ImportError: 16 | print('You must install pywin32') 17 | hexchat.command('timer 0.1 py unload iTunes') 18 | 19 | def np_cb(word, word_eol, userdata): 20 | try: 21 | itunes = win32com.client.Dispatch('iTunes.Application') 22 | except pywintypes.com_error: 23 | print("itunes: Failed to communicate with itunes. Note that this requires admin access and you should have had a UAC prompt.") 24 | return hexchat.EAT_ALL 25 | 26 | # TODO: Settings 27 | try: 28 | track = itunes.CurrentTrack 29 | hexchat.command('me is now playing {} by {} on {}'.format(track.Name, track.Artist, track.Album)) 30 | except AttributeError: 31 | print('No song was found playing.') 32 | 33 | return hexchat.EAT_ALL 34 | 35 | def unload_cb(userdata): 36 | if loaded: print(__module_name__, 'version', __module_version__, 'unloaded.') 37 | 38 | hexchat.hook_command('np', np_cb) 39 | hexchat.hook_unload(unload_cb) 40 | -------------------------------------------------------------------------------- /HexChat/lastfm.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import json 5 | 6 | if sys.version_info[0] == 2: 7 | import urllib2 as urllib_error 8 | import urllib as urllib_request 9 | else: 10 | import urllib.error as urllib_error 11 | import urllib.request as urllib_request 12 | 13 | import hexchat 14 | 15 | __module_name__ = 'lastfm' 16 | __module_author__ = 'TingPing' 17 | __module_version__ = '1' 18 | __module_description__ = 'Tell others what you are playing on last.fm' 19 | 20 | lfm_help = """Lastfm Usage: 21 | LFM 22 | LFM -e""" 23 | 24 | USERNAME = hexchat.get_pluginpref('lfm_username') 25 | KEY = '4847f738e6b34c0dc20b13fe42ea008e' 26 | 27 | def print_nowplaying(track, echo=False): 28 | try: 29 | title = track['name'] 30 | artist = track['artist']['#text'] 31 | album = track['album']['#text'] 32 | if sys.version_info[0] == 2: 33 | title = title.encode('utf-8') 34 | artist = artist.encode('utf-8') 35 | album = album.encode('utf-8') 36 | except KeyError: 37 | print('Lastfm: Song info not found') 38 | return 39 | 40 | if echo: 41 | cmd = 'echo Lastfm: {} by {} on {}.'.format(title, artist, album) 42 | elif hexchat.get_pluginpref('lfm_say'): 43 | cmd = 'say Now playing {} by {} on {}.'.format(title, artist, album) 44 | else: 45 | cmd = 'me is now playing {} by {} on {}.'.format(title, artist, album) 46 | 47 | hexchat.command(cmd) 48 | 49 | def get_track(): 50 | url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecentTracks&user={}&api_key={}&format=json'.format(USERNAME, KEY) 51 | try: 52 | response = urllib_request.urlopen(url) 53 | text = response.read().decode('utf-8') 54 | response.close() 55 | except urllib_error.HTTPError: 56 | return 57 | 58 | data = json.loads(text) 59 | 60 | try: 61 | track = data['recenttracks']['track'][0] 62 | if track['@attr']['nowplaying']: 63 | return track 64 | except (IndexError, KeyError): 65 | return 66 | 67 | def lfm_cb(word, word_eol, userdata): 68 | global USERNAME 69 | echo = False 70 | 71 | if len(word) == 2: 72 | if word[1] == '-e': 73 | echo = True 74 | else: 75 | USERNAME = word[1] 76 | hexchat.set_pluginpref('lfm_username', USERNAME) 77 | print('Lastfm: Username set to {}'.format(USERNAME)) 78 | return hexchat.EAT_ALL 79 | 80 | if not USERNAME: 81 | print('Lastfm: No username set, use /lfm to set it') 82 | return hexchat.EAT_ALL 83 | 84 | track = get_track() 85 | if not track: 86 | print('Lastfm: No song currently playing') 87 | else: 88 | print_nowplaying(track, echo) 89 | 90 | return hexchat.EAT_ALL 91 | 92 | hexchat.hook_command('lfm', lfm_cb, help=lfm_help) 93 | -------------------------------------------------------------------------------- /HexChat/logpm.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('LogPM', '2', 'Automatically log private queries') 3 | 4 | hexchat.hook_print('Open Context', function (args) 5 | -- We only want queries 6 | if hexchat.props['type'] ~= 3 then 7 | return 8 | end 9 | 10 | -- Ignore empty tabs, znc queries, and scripting consoles 11 | if not hexchat.get_info('channel'):match('^[%*%(>]') then 12 | hexchat.command('chanopt -quiet text_logging on') 13 | end 14 | end) 15 | -------------------------------------------------------------------------------- /HexChat/masshighlightignore.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('MassHighlightIgnore', '3', 'Ignore mass highlight spam') 3 | 4 | if unpack == nil then 5 | unpack = table.unpack -- fix lua 5.2 6 | end 7 | 8 | local MAX_COUNT = 4 9 | 10 | -- http://lua-users.org/wiki/SplitJoin 11 | local function split(str) 12 | local t = {} 13 | for i in string.gmatch(str, "%S+") do 14 | t[#t + 1] = i 15 | end 16 | return t 17 | end 18 | 19 | local function nick_in_list (nick, list) 20 | for _, word in pairs(list) do 21 | if hexchat.nickcmp(word, nick) == 0 then 22 | return true 23 | end 24 | end 25 | return false 26 | end 27 | 28 | local function is_mass_highlight (message) 29 | local count = 0 30 | local words = split(message) 31 | 32 | for user in hexchat.props.context:iterate('users') do 33 | if nick_in_list(user.nick, words) then 34 | count = count + 1 35 | if count == MAX_COUNT then 36 | return true 37 | end 38 | end 39 | end 40 | 41 | return false 42 | end 43 | 44 | local function ignore_mass_hilight (args, attrs, event) 45 | if is_mass_highlight(args[2]) then 46 | hexchat.emit_print_attrs(attrs, event, unpack(args)) 47 | return hexchat.EAT_ALL 48 | end 49 | end 50 | 51 | hexchat.hook_print_attrs('Channel Msg Hilight', function (args, attrs) 52 | return ignore_mass_hilight(args, attrs, 'Channel Message') 53 | end, hexchat.PRI_HIGHEST) 54 | 55 | hexchat.hook_print('Channel Action Hilight', function (args, attrs) 56 | return ignore_mass_hilight(args, attrs, 'Channel Action') 57 | end, hexchat.PRI_HIGHEST) 58 | -------------------------------------------------------------------------------- /HexChat/mymsg.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('MyMessage', '2', 'Properly show your own messages in ZNC playback') 3 | 4 | local function get_server_ctx() 5 | local id = hexchat.prefs['id'] 6 | for chan in hexchat.iterate('channels') do 7 | if chan.type == 1 and chan.id == id then 8 | return chan.context 9 | end 10 | end 11 | return hexchat.props.context 12 | end 13 | 14 | hexchat.hook_print('Capability List', function (args) 15 | if args[2]:find('znc.in/self%-message') then 16 | hexchat.command('CAP REQ znc.in/self-message') 17 | 18 | local ctx = get_server_ctx() 19 | hexchat.hook_timer(1, function () 20 | -- Emit right after this event 21 | if ctx:set() then 22 | hexchat.emit_print('Capability Request', 'znc.in/self-message') 23 | end 24 | end) 25 | end 26 | end) 27 | 28 | local function prefix_is_channel (prefix) 29 | local chantypes = hexchat.props['chantypes'] 30 | for i = 1, #chantypes do 31 | if chantypes:sub(i, i) == prefix then 32 | return true 33 | end 34 | end 35 | return false 36 | end 37 | 38 | hexchat.hook_server_attrs('PRIVMSG', function (word, word_eol, attrs) 39 | -- Only want private messages 40 | if prefix_is_channel(word[3]:sub(1, 1)) then 41 | return 42 | end 43 | 44 | local mynick = hexchat.get_info('nick') 45 | local sender = word[1]:match('^:([^!]+)') 46 | local recipient = word[3] 47 | 48 | if hexchat.nickcmp(sender, mynick) == 0 and hexchat.nickcmp(recipient, mynick) ~= 0 then 49 | hexchat.command('query -nofocus ' .. recipient) 50 | local ctx = hexchat.find_context(hexchat.get_info('network'), recipient) 51 | local message = word_eol[4] 52 | if message:sub(1, 1) == ':' then 53 | message = message:sub(2) 54 | end 55 | 56 | if message:sub(1, 8) == '\001ACTION ' then 57 | local action = message:sub(9, #message-1) 58 | ctx:emit_print_attrs(attrs, 'Your Action', mynick, action) 59 | else 60 | ctx:emit_print_attrs(attrs, 'Your Message', mynick, message) 61 | end 62 | 63 | return hexchat.EAT_ALL 64 | end 65 | end) 66 | -------------------------------------------------------------------------------- /HexChat/nowplaying.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | local lgi = require('lgi') 3 | local GLib = lgi.require('GLib') 4 | local Gio = lgi.require('Gio') 5 | 6 | hexchat.register('NowPlaying', '4', 'Announce songs from MPRIS2 clients') 7 | 8 | --[[ 9 | Modify this template string for a different command. 10 | 11 | Syntax: 12 | $field - replaced with the value of field; 13 | only alphanumeric characters are recognized 14 | ${field} - replaced with the value of field; 15 | recognizes all characters but `=` (denotes default value) and `}` 16 | (closes template replacement) 17 | ${field=default} - replaced with the value of field or "default" if it is nil 18 | $$ - a literal dollar sign 19 | 20 | Available Fields: 21 | ${xesam:*}, ${mpris:*} 22 | - As specified in https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata 23 | $title - short for ${xesam:title} 24 | $album - short for ${xesam:album} 25 | $artist - aggregated string from the 'xesam:artist' list provided by metadata 26 | $albumArtist - same as $artist for 'xesam:albuArtist' 27 | $position - the current playback position as a formatted timestamp 28 | $length - ${mpris:length} as a formatted timestamp 29 | $player - name of the player 30 | ]] 31 | local COMMAND_TEMPLATE = ( 32 | "me is now playing" 33 | .. " \002${title=Unknown Title}\002" 34 | .. " by" 35 | .. " \002${artist=Unknown Artist}\002" 36 | .. " [$position/$length]" 37 | ) 38 | 39 | 40 | local bus 41 | local cancellable = Gio.Cancellable() 42 | 43 | Gio.bus_get(Gio.BusType.SESSION, cancellable, function (object, result) 44 | local connection, err = Gio.bus_get_finish(result) 45 | 46 | if err then 47 | local front_ctx = hexchat.find_context(nil, nil) 48 | front_ctx:print('NP: Error connecting to dbus: ' .. tostring(err)) 49 | else 50 | bus = connection 51 | end 52 | end) 53 | 54 | local function get_players (callback) 55 | bus:call('org.freedesktop.DBus', -- bus 56 | '/org/freedesktop/DBus', -- object 57 | 'org.freedesktop.DBus', -- interface 58 | 'ListNames', -- method 59 | nil, -- params 60 | GLib.VariantType('(as)'), -- return type 61 | Gio.DBusCallFlags.NONE, -1, 62 | cancellable, 63 | function (connection, result) 64 | local ret, err = connection:call_finish(result) 65 | 66 | if err then 67 | -- print('NP: Error ' .. tostring(err)) 68 | return 69 | elseif #ret ~= 1 then 70 | return 71 | end 72 | 73 | local players = {} 74 | local array = ret.value[1] 75 | for i = 1, #array do 76 | local player_name = array[i]:match('^org%.mpris%.MediaPlayer2%.(.+)') 77 | if player_name then 78 | players[#players + 1] = player_name 79 | end 80 | end 81 | 82 | if #players == 0 then 83 | callback(nil) 84 | else 85 | callback(players) 86 | end 87 | end) 88 | end 89 | 90 | local function get_property (player, prop_path, callback) 91 | -- https://specifications.freedesktop.org/mpris-spec/latest/ 92 | bus:call('org.mpris.MediaPlayer2.' .. player, -- bus 93 | '/org/mpris/MediaPlayer2', -- path 94 | 'org.freedesktop.DBus.Properties', -- interface 95 | 'Get', -- method 96 | GLib.Variant('(ss)', prop_path), -- params 97 | GLib.VariantType('(v)'), -- return type 98 | Gio.DBusCallFlags.NONE, -1, 99 | cancellable, 100 | function (connection, result) 101 | local ret, err = connection:call_finish(result) 102 | 103 | if err then 104 | print('NP: Error ' .. tostring(err)) 105 | return 106 | elseif #ret ~= 1 then 107 | return 108 | end 109 | 110 | callback(ret[1].value) 111 | end) 112 | end 113 | 114 | local function get_metadata(player, callback) 115 | get_property(player, {'org.mpris.MediaPlayer2.Player', 'Metadata'}, callback) -- a{sv} 116 | end 117 | 118 | local function get_position(player, callback) 119 | get_property(player, {'org.mpris.MediaPlayer2.Player', 'Position'}, callback) -- x 120 | end 121 | 122 | local function format_timestamp (microsecs) 123 | if microsecs == nil then 124 | return 125 | end 126 | local secs = math.floor(microsecs / 1000000) 127 | local mins = math.floor(secs / 60) 128 | local hours = math.floor(mins / 60) 129 | local str = string.format("%d:%02d", mins % 60, secs % 60) 130 | if hours > 0 then 131 | str = string.format("%d:%02d:%02d", hours, mins % 60, secs % 60) 132 | end 133 | return str 134 | end 135 | 136 | local function template_string (template, replacements) 137 | template = template:gsub("%$%$", "\000") -- $$ -> \000 138 | template = template:gsub("%$([%w_]+)", replacements) -- $field 139 | template = template:gsub("%${([^}]+)}", function (field) -- ${field}; ${field=default} 140 | field, default = field:match('(.+)=(.*)') or field 141 | repl = replacements[field] 142 | if repl == nil and repl ~= nil then 143 | repl = default or "" 144 | end 145 | return repl 146 | end) 147 | template = template:gsub("\000", "$") -- \000 -> $ 148 | return template 149 | end 150 | 151 | local function print_nowplaying (player) 152 | local original_context = hexchat.props.context 153 | 154 | get_position(player, function (position) -- x 155 | get_metadata(player, function (metadata) -- a{sv} 156 | if not original_context:set() then -- check if context still exists 157 | print("NP: original context has been closed") 158 | return 159 | end 160 | 161 | -- I don't have the desire to track this down atm but the template system is 162 | -- broken in luajit so just bypass it for now... 163 | if type(jit) == 'table' then 164 | local command = string.format('me is now playing %s by %s', metadata['xesam:title'], metadata['xesam:artist'][1]) 165 | hexchat.command(command) 166 | else 167 | local replacements = metadata -- there's no built-in function to make a shallow copy 168 | 169 | -- aggregate artist and albumArtist fields 170 | for _, key in pairs({'artist', 'albumArtist'}) do 171 | local source = metadata['xesam:' .. key] 172 | if source then 173 | replacements[key] = "" 174 | for i, item in ipairs(source) do 175 | if i ~= 1 then 176 | replacements[key] = replacements[key] .. " & " 177 | end 178 | replacements[key] = replacements[key] .. item 179 | end 180 | end 181 | end 182 | -- formatted timestamps 183 | replacements['position'] = format_timestamp(position) 184 | replacements['length'] = format_timestamp(metadata['mpris:length']) 185 | if replacements['length'] == nil then 186 | replacements['length'] = '(radio stream)' 187 | end 188 | replacements['title'] = metadata['xesam:title'] 189 | replacements['album'] = metadata['xesam:album'] 190 | -- other 191 | replacements['player'] = player 192 | 193 | hexchat.command(template_string(COMMAND_TEMPLATE, replacements)) 194 | end 195 | end) 196 | end) 197 | end 198 | 199 | hexchat.hook_command('np', function (word, word_eol) 200 | if not bus then 201 | print('NP: Connection to dbus not yet established') 202 | return hexchat.EAT_ALL 203 | end 204 | 205 | local original_context = hexchat.props.context 206 | get_players (function (players) 207 | if not original_context:set() then 208 | return -- If the tab was closed just don't care 209 | end 210 | 211 | if not players then 212 | print('NP: No player found running.') 213 | elseif #word > 1 then 214 | local player = word[2]:lower() 215 | 216 | for _, name in pairs(players) do 217 | if player == name:lower() then 218 | print_nowplaying(name) 219 | return 220 | end 221 | end 222 | 223 | print('NP: Player ' .. word[2] .. ' not found.') 224 | elseif #players == 1 then 225 | print_nowplaying(players[1]) 226 | else 227 | print('NP: You have multiple players running, please specify a name:\n\t' .. table.concat(players, ', ')) 228 | end 229 | end) 230 | 231 | return hexchat.EAT_ALL 232 | end, 'NP [player]') 233 | 234 | hexchat.hook_unload (function () 235 | -- FIXME: Seems this can possibly crash 236 | -- /np 237 | -- /lua unload ~/.config/hexchat/addons/nowplaying.lua 238 | cancellable:cancel() 239 | end) 240 | -------------------------------------------------------------------------------- /HexChat/playback.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | local lgi = require('lgi') 3 | local GLib = lgi.require('GLib') 4 | 5 | hexchat.register('Playback', '1', "Integration with ZNC's Playback module") 6 | 7 | --[[ 8 | This should behave like this: 9 | 10 | On connect (end of MOTD): 11 | if new server, play all 12 | if old server, play all after latest timestamp 13 | On close query: 14 | clear all 15 | On new message: 16 | update latest timestamp 17 | ]] 18 | 19 | local CAP_NAME = 'znc.in/playback' 20 | local servers = {} -- Table of id to timestamp 21 | 22 | -- Request capability 23 | hexchat.hook_print('Capability List', function (args) 24 | if args[2]:find(CAP_NAME) then 25 | hexchat.command('quote CAP REQ :' .. CAP_NAME) 26 | 27 | local ctx = hexchat.props.context 28 | hexchat.hook_timer(1, function () 29 | -- Emit this event right after the current one 30 | if ctx:set() then 31 | hexchat.emit_print('Capability Request', CAP_NAME) 32 | end 33 | end) 34 | end 35 | end) 36 | 37 | -- Capability supported 38 | hexchat.hook_print('Capability Acknowledgement', function (args) 39 | local id = hexchat.prefs['id'] 40 | if args[2]:find(CAP_NAME) and not servers[id] then 41 | servers[id] = 0 -- New server 42 | end 43 | end) 44 | 45 | local function play_history() 46 | local timestamp = servers[hexchat.prefs['id']] 47 | 48 | if timestamp then 49 | hexchat.command('quote PRIVMSG *playback :play * ' .. tostring(timestamp)) 50 | end 51 | end 52 | 53 | -- On RPL_ENDOFMOTD or ERR_NOMOTD play history 54 | hexchat.hook_server('376', function (word, word_eol) 55 | play_history() 56 | end) 57 | 58 | hexchat.hook_server('422', function (word, word_eol) 59 | play_history() 60 | end) 61 | 62 | -- Remove history when closed 63 | hexchat.hook_print('Close Context', function (args) 64 | local id = hexchat.prefs['id'] 65 | local timestamp = servers[id] 66 | if not timestamp then 67 | return 68 | end 69 | 70 | local ctx_type = hexchat.props['type'] 71 | if ctx_type == 3 then -- Dialog 72 | hexchat.command('quote PRIVMSG *playback :clear ' .. hexchat.get_info('channel')) 73 | elseif ctx_type == 1 then -- Server 74 | servers[id] = nil 75 | end 76 | end) 77 | 78 | -- Store the timestamp of the latest message on the server 79 | hexchat.hook_server_attrs('PRIVMSG', function (word, word_eol, attrs) 80 | local id = hexchat.prefs['id'] 81 | if servers[id] then 82 | servers[id] = GLib.get_real_time() / 1000000 -- epoch in seconds with milisecond precision UTC 83 | end 84 | end, hexchat.PRI_LOWEST) 85 | -------------------------------------------------------------------------------- /HexChat/playbackstamps.js: -------------------------------------------------------------------------------- 1 | SCRIPT_NAME = 'PlaybackStamps'; 2 | SCRIPT_VER = '1'; 3 | SCRIPT_DESC = 'Prints date on older playback messages'; 4 | // I wrote this in Python first but this is noticably faster 5 | 6 | const events = ['Channel Message', 'Channel Msg Hilight', 7 | 'Channel Action', 'Channel Action Hilight', 8 | 'Your Action', 'Your Message']; 9 | 10 | var edited = false; 11 | 12 | function is_today (event_time) 13 | { 14 | let now = new Date(); 15 | let etime = new Date(event_time); // Duplicate since we edit it later 16 | 17 | if (now.getTime() - etime.getTime() > 86400000) 18 | return false; // Was over 24hrs ago 19 | 20 | // Set time to 0, we only care about date 21 | if (etime.setHours(0,0,0,0) != now.setHours(0,0,0,0)) 22 | return false; // Different date 23 | 24 | return true; 25 | } 26 | 27 | function msg_cb(word, event_time, event) 28 | { 29 | if (edited || is_today(event_time)) 30 | return EAT_NONE; 31 | 32 | let format = get_prefs('stamp_text_format'); 33 | command('set -quiet stamp_text_format' + ' %m-%d ' + format); 34 | 35 | edited = true; 36 | emit_print_at(event_time, event, word[0], word[1]); 37 | edited = false; 38 | 39 | command('set -quiet stamp_text_format ' + format); 40 | 41 | return EAT_ALL; 42 | } 43 | 44 | for each (let event in events) { 45 | hook_print(event, msg_cb, event, PRI_LOWEST); 46 | } 47 | -------------------------------------------------------------------------------- /HexChat/pmcolor.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('PMColor', '1', 'Color PM tabs like highlights') 3 | 4 | for _, event in pairs({'Private Message to Dialog', 'Private Action to Dialog'}) do 5 | hexchat.hook_print(event, function (args) 6 | hexchat.command('gui color 3') 7 | end) 8 | end 9 | -------------------------------------------------------------------------------- /HexChat/pytabcomplete.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import hexchat 3 | 4 | __module_name__ = "PythonTabComplete" 5 | __module_author__ = "TingPing, FichteFoll" 6 | __module_version__ = "0.3" 7 | __module_description__ = "Tab-completes module attributes in Interactive Console" 8 | 9 | last_index = None 10 | last_completes = None 11 | last_text = None 12 | last_pos = None 13 | 14 | exec_scope = {} 15 | 16 | 17 | def keypress_cb(word, word_eol, userdata): 18 | global last_index 19 | global last_completes 20 | global last_text 21 | global last_pos 22 | 23 | if not word[0] == '65289': # Tab 24 | return 25 | if not hexchat.get_info('channel') == '>>python<<': 26 | return 27 | shift_key_is_down = bool(int(word[1]) & 1) # check if shift modifier is hold 28 | 29 | text = hexchat.get_info('inputbox') 30 | pos = hexchat.get_prefs('state_cursor') 31 | if not text: 32 | return 33 | 34 | base = text[:pos].split(' ')[-1] 35 | module, _, prefix = base.rpartition('.') 36 | if not module: 37 | # can not dir() the console's interpreter state sadly 38 | return 39 | 40 | if last_text != text or last_pos != pos: 41 | # new completion context 42 | try: 43 | exec('import {}'.format(module), exec_scope) # Has to be imported to dir() it 44 | completions = eval('dir({})'.format(module), exec_scope) 45 | except (NameError, SyntaxError, ImportError): 46 | return 47 | completions = [c for c in completions if c.startswith(prefix)] 48 | if not completions: 49 | return 50 | index = 0 51 | else: 52 | # same context, insert next completion 53 | completions = last_completes 54 | direction = 1 if not shift_key_is_down else -1 55 | index = (last_index + direction) % len(completions) 56 | 57 | complete_text = completions[index] 58 | 59 | new_text = text[:pos - len(prefix)] + complete_text + text[pos:] 60 | new_pos = pos - len(prefix) + len(complete_text) 61 | 62 | hexchat.command('settext {}'.format(new_text)) 63 | hexchat.command('setcursor {}'.format(new_pos)) 64 | 65 | last_index = index 66 | last_completes = completions 67 | last_text = new_text 68 | last_pos = new_pos 69 | 70 | 71 | def unload_cb(userdata): 72 | print(__module_name__, 'version', __module_version__, 'unloaded.') 73 | 74 | 75 | hexchat.hook_print('Key Press', keypress_cb) 76 | hexchat.hook_unload(unload_cb) 77 | print(__module_name__, 'version', __module_version__, 'loaded.') 78 | -------------------------------------------------------------------------------- /HexChat/readme.md: -------------------------------------------------------------------------------- 1 | ## Notes 2 | 3 | All scripts are under the [MIT license](/license). 4 | 5 | ### Lua 6 | 7 | Any lua scripts will require the plugin which is included with HexChat 2.12.1+. These are recommended 8 | over the Python versions as they typically have the same functionality but are far lighter on memory 9 | and more portable. 10 | 11 | ### Python 12 | 13 | Most of the Python scripts will work in Python 2.7 or 3.3+ on Windows or Linux and many require HexChat 2.9.6+ 14 | as a lot was changed in that release. 15 | 16 | ### JavaScript 17 | 18 | Any javascript scripts will require the plugin I wrote that can be found [here](https://github.com/TingPing/hexchat-javascript). 19 | 20 | -------------------------------------------------------------------------------- /HexChat/requiresasl.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('RequireSASL', '1', 'Disconnect if SASL fails') 3 | 4 | hexchat.hook_print('SASL Response', function (args) 5 | if args[2] == '904' then 6 | -- Delayed so print happens first 7 | hexchat.command('timer .1 discon') 8 | end 9 | end) 10 | -------------------------------------------------------------------------------- /HexChat/screensaver.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | local lgi = require "lgi" 3 | local Gio = lgi.Gio 4 | 5 | hexchat.register("screensaver", "1.0", "Sets user away when the GNOME screensaver is activated") 6 | 7 | local bus = Gio.bus_get_sync(Gio.BusType.SESSION) 8 | 9 | local function on_active_changed(conn, sender, obj_path, interface, signal, param) 10 | if param[1] then 11 | hexchat.command("allserv away") 12 | else 13 | hexchat.command("allserv back") 14 | end 15 | end 16 | 17 | bus:signal_subscribe(nil, "org.gnome.ScreenSaver", "ActiveChanged", nil, nil, 18 | Gio.DBusSignalFlags.NONE, on_active_changed) 19 | -------------------------------------------------------------------------------- /HexChat/screenshot.txt: -------------------------------------------------------------------------------- 1 | recv :holmes.freenode.net CAP Theseus ACK :server-time away-notify 2 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 332 Theseus #athenscentral :Welcome to "A Midsummer Night's Dream". Sit back, take up a chair, and enjoy the show 3 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 333 Theseus #athenscentral Theseus 1206481712 4 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 353 Theseus = #athenscentral :@Theseus +HIPPO-lyta +Pyramus Wall DEMETRIUS lysander hermia Helena Egeus 5 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 366 Theseus #athenscentral :End of /NAMES list. 6 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 352 Theseus #athenscentral francis flute.bellows-mender.com irc.freenode.net Thisbe G :0 Francis Flute 7 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 352 Theseus #athenscentral ~nick dsl-192-168-1-50.play.dream.net irc.freenode.net Pyramus G :0 Nick Bottom 8 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 352 Theseus #athenscentral duke city.athens.gov irc.freenode.net Theseus H :0 Theseus, Duke of Athens 9 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 352 Theseus #athenscentral snout i.am.a.tinker.it irc.freenode.net Wall H :0 Tom Snout 10 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 352 Theseus #athenscentral ~youth i.think.i.love.helena.org DEMETRIUS H :0 Demetrius 11 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 352 Theseus #athenscentral queen leader.amazons.org HIPPO-lyta H :0 Hippolyta, Queen of the Amazons 12 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 352 Theseus #athenscentral ~dashing hermia.is.my.love.net lysander H :0 I love Hermia 13 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 315 Theseus #athenscentral :End of /WHO list. 14 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 324 Theseus #athenscentral +tnc 15 | recv @time=2013-09-21T02:00:00.000Z :holmes.freenode.net 329 Theseus #athenscentral 1177267404 16 | recv @time=2013-09-21T02:00:01.000Z :Helena!helena@helena.gov AWAY :AFK 17 | recv @time=2013-09-21T02:00:01.000Z :Egeus!egeus@ege.us AWAY :AFK 18 | recv @time=2013-09-21T02:00:02.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :The wall, methinks, being sensible, should curse again. 19 | recv @time=2013-09-21T02:00:05.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :No, in truth, sir, he should not. 'Deceiving me' is Thisby's cue: she is to enter now, and I am to spy her through the wall. You shall see, it will fall pat as I told you. Yonder she comes. 20 | recv @time=2013-09-21T02:00:10.000Z :Thisbe!francis@flute.bellows-mender.com JOIN :#athenscentral 21 | recv @time=2013-09-21T02:00:18.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :O wall, full often hast thou heard my moans, For parting my fair Pyramus and me! 22 | recv @time=2013-09-21T02:00:40.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :My cherry lips have often kiss'd thy stones, Thy stones with lime and hair knit up in thee. 23 | recv @time=2013-09-21T02:00:56.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :I see a voice: now will I to the chink, To spy an I can hear my Thisby's face. Thisby! 24 | recv @time=2013-09-21T02:00:59.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :ACTION runs up to wall and looks for a hole 25 | recv @time=2013-09-21T02:01:07.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :My love thou art, my love I think. 26 | recv @time=2013-09-21T02:01:20.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :Think what thou wilt, I am thy lover's grace; And, like Limander, am I trusty still. 27 | recv @time=2013-09-21T02:01:28.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :And I like Helen, till the Fates me kill. 28 | recv @time=2013-09-21T02:01:33.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :Not Shafalus to Procrus was so true. 29 | recv @time=2013-09-21T02:01:39.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :As Shafalus to Procrus, I to you. 30 | recv @time=2013-09-21T02:01:45.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :O kiss me through the hole of this vile wall! 31 | recv @time=2013-09-21T02:01:49.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :ACTION attempts to kiss Pyramus, but is blocked by Wall 32 | recv @time=2013-09-21T02:01:54.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :I kiss the wall's hole, not your lips at all. 33 | recv @time=2013-09-21T02:02:00.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :Wilt thou at Ninny's tomb meet me straightway? 34 | recv @time=2013-09-21T02:02:06.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :'Tide life, 'tide death, I come without delay. 35 | recv @time=2013-09-21T02:02:10.000Z :Thisbe!francis@flute.bellows-mender.com PART #athenscentral : 36 | recv @time=2013-09-21T02:02:11.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PART #athenscentral :"And now to find Ninny's tomb" 37 | recv @time=2013-09-21T02:02:13.000Z :Wall!snout@i.am.a.tinker.it PRIVMSG #athenscentral :Thus have I, Wall, my part discharged so; 38 | recv @time=2013-09-21T02:02:18.000Z :Wall!snout@i.am.a.tinker.it QUIT :Quit: And, being done, thus Wall away doth go. 39 | recv @time=2013-09-21T02:02:24.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :Now is the mural down between the two neighbours. 40 | recv @time=2013-09-21T02:02:30.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :No remedy, my lord, when walls are so wilful to hear without warning. 41 | recv @time=2013-09-21T02:02:37.000Z :HIPPO-lyta!queen@leader.amazons.org PRIVMSG #athenscentral :This is the silliest stuff that ever I heard. 42 | recv @time=2013-09-21T02:02:44.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :The best in this kind are but shadows; and the worst are no worse, if imagination amend them. 43 | recv @time=2013-09-21T02:02:49.000Z :HIPPO-lyta!queen@leader.amazons.org PRIVMSG #athenscentral :It must be your imagination then, and not theirs. 44 | recv @time=2013-09-21T02:02:58.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :If we imagine no worse of them than they of themselves, they may pass for excellent men. 45 | recv @time=2013-09-21T02:03:00.000Z :I_R_LION!snug@lol.imma.joiner JOIN :#athenscentral 46 | recv @time=2013-09-21T02:03:02.000Z :m00nshine!~starveling@172.28.42.189 JOIN :#athenscentral 47 | recv @time=2013-09-21T02:03:05.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :Here come two noble beasts in, a man and a lion. 48 | recv @time=2013-09-21T02:03:10.000Z :I_R_LION!snug@lol.imma.joiner PRIVMSG #athenscentral :You, ladies, you, whose gentle hearts do fear The smallest monstrous mouse that creeps on floor, May now perchance both quake and tremble here, When lion rough in wildest rage doth roar. 49 | recv @time=2013-09-21T02:03:15.000Z :I_R_LION!snug@lol.imma.joiner PRIVMSG #athenscentral :Then know that I, one Snug the joiner, am 50 | recv @time=2013-09-21T02:03:19.000Z :I_R_LION!snug@lol.imma.joiner PRIVMSG #athenscentral :A lion-fell, nor else no lion's dam; For, if I should as lion come in strife Into this place, 'twere pity on my life. 51 | recv @time=2013-09-21T02:03:26.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :A very gentle beast, of a good conscience. 52 | recv @time=2013-09-21T02:03:29.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :The very best at a beast, my lord, that e'er I saw. 53 | recv @time=2013-09-21T02:03:36.000Z :lysander!~dashing@hermia.is.my.love.net PRIVMSG #athenscentral :This lion is a very fox for his valour. 54 | recv @time=2013-09-21T02:03:40.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :True; and a goose for his discretion. 55 | recv @time=2013-09-21T02:03:48.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :Not so, my lord; for his valour cannot carry his discretion; and the fox carries the goose. 56 | recv @time=2013-09-21T02:03:59.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :His discretion, I am sure, cannot carry his valour; for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon. 57 | recv @time=2013-09-21T02:04:05.000Z :m00nshine!~starveling@172.28.42.189 PRIVMSG #athenscentral :This lanthorn doth the horned moon present;-- 58 | recv @time=2013-09-21T02:04:08.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :He should have worn the horns on his head. 59 | recv @time=2013-09-21T02:04:14.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :He is no crescent, and his horns are invisible within the circumference. 60 | recv @time=2013-09-21T02:04:23.000Z :m00nshine!~starveling@172.28.42.189 PRIVMSG #athenscentral :This lanthorn doth the horned moon present; Myself the man i' the moon do seem to be. 61 | recv @time=2013-09-21T02:04:30.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :This is the greatest error of all the rest: the man should be put into the lanthorn. How is it else the man i' the moon? 62 | recv @time=2013-09-21T02:04:38.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :He dares not come there for the candle; for, you see, it is already in snuff. 63 | recv @time=2013-09-21T02:04:45.000Z :HIPPO-lyta!queen@leader.amazons.org PRIVMSG #athenscentral :I am aweary of this moon: would he would change! 64 | recv @time=2013-09-21T02:04:49.000Z :HIPPO-lyta!queen@leader.amazons.org PRIVMSG Theseus :PING 65 | recv @time=2013-09-21T02:04:50.000Z :HIPPO-lyta!queen@leader.amazons.org PRIVMSG Theseus :ping 66 | recv @time=2013-09-21T02:04:50.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :It appears, by his small light of discretion, that he is in the wane; but yet, in courtesy, in all reason, we must stay the time. 67 | recv @time=2013-09-21T02:04:50.000Z :m00nshine!~starveling@172.28.42.189 PRIVMSG #athenscentral :ACTION is frustrated with the peanut gallery 68 | recv @time=2013-09-21T02:04:56.000Z :lysander!~dashing@hermia.is.my.love.net PRIVMSG #athenscentral :Proceed, Moon. 69 | recv @time=2013-09-21T02:05:00.000Z :m00nshine!~starveling@172.28.42.189 PRIVMSG #athenscentral :All that I have to say, is, to tell you that the lanthorn is the moon; I, the man in the moon; this thorn-bush, my thorn-bush; 70 | recv @time=2013-09-21T02:05:10.000Z :m00nshine!~starveling@172.28.42.189 PRIVMSG #athenscentral :AND THIS DOG, MY DOG! 71 | recv @time=2013-09-21T02:05:15.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :Why, all these should be in the lanthorn; for all these are in the moon. 72 | recv @time=2013-09-21T02:05:16.000Z :Thisbe!francis@flute.bellows-mender.com JOIN :#athenscentral 73 | recv @time=2013-09-21T02:05:20.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :But, silence! here comes Thisbe. 74 | recv @time=2013-09-21T02:05:30.000Z :Thisbe!francis@flute.bellows-mender.com PRIVMSG #athenscentral :This is old Ninny's tomb. Where is my love? 75 | recv @time=2013-09-21T02:05:36.000Z :I_R_LION!snug@lol.imma.joiner PRIVMSG #athenscentral :ACTION ROARS loudly 76 | recv @time=2013-09-21T02:05:40.000Z :Thisbe!francis@flute.bellows-mender.com PART #athenscentral :"eek" 77 | recv @time=2013-09-21T02:05:48.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :Well roared, Lion. 78 | recv @time=2013-09-21T02:05:55.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :Well run, Thisbe. 79 | recv @time=2013-09-21T02:06:01.000Z :HIPPO-lyta!queen@leader.amazons.org PRIVMSG #athenscentral :Well shone, Moon. Truly, the moon shines with a good grace. 80 | recv @time=2013-09-21T02:06:09.000Z :I_R_LION!snug@lol.imma.joiner QUIT :Quit: I R Have Thisbe's MANTLE! 81 | recv @time=2013-09-21T02:06:12.000Z :I_R_LION!snug@lol.imma.joiner PRIVMSG Theseus :Theseus 82 | recv @time=2013-09-21T02:06:14.000Z :Theseus!duke@city.athens.gov PRIVSMG #athenscentral :Well moused, Lion. 83 | recv @time=2013-09-21T02:06:18.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org NOTICE Theseus :Why did you hire these fools? 84 | recv @time=2013-09-21T02:06:20.000Z :lysander!~dashing@hermia.is.my.love.net PRIVMSG #athenscentral :And so the lion vanished. 85 | recv @time=2013-09-21T02:06:20.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net JOIN :#athenscentral 86 | recv @time=2013-09-21T02:06:20.000Z :Theseus!duke@city.athens.gov MODE #athenscentral :+v Pyramus 87 | recv @time=2013-09-21T02:06:25.000Z :DEMETRIUS!~youth@i.think.i.love.helena.org PRIVMSG #athenscentral :And then came Pyramus. 88 | recv @time=2013-09-21T02:06:36.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :Sweet Moon, I thank thee for thy sunny beams; I thank thee, Moon, for shining now so bright; For, by thy gracious, golden, glittering gleams, I trust to take of truest Thisby sight. 89 | recv @time=2013-09-21T02:06:44.000Z :Pyramus!~nick@dsl-192-168-1-50.play.dream.net NICK :Pyramus|Angsty 90 | recv @time=2013-09-21T02:06:48.000Z :Pyramus|Angsty!~nick@dsl-192-168-1-50.play.dream.net PRIVMSG #athenscentral :But stay, O spite! But mark, poor knight, What dreadful dole is here! 91 | -------------------------------------------------------------------------------- /HexChat/script.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import sys 5 | import json 6 | import threading 7 | 8 | THREADS_SUPPORTED = True 9 | if sys.version_info[0] == 2: 10 | import urllib2 as urllib_error 11 | import urllib as urllib_request 12 | 13 | # Avoid threads on py2/win32. This will lockup HexChat 14 | if sys.platform == 'win32': 15 | THREADS_SUPPORTED = False 16 | else: 17 | import urllib.error as urllib_error 18 | import urllib.request as urllib_request 19 | 20 | import hexchat 21 | 22 | __module_name__ = 'Script' 23 | __module_author__ = 'TingPing' 24 | __module_version__ = '6' 25 | __module_description__ = 'Manage scripts' 26 | 27 | # TODO: 28 | # add preference for adding repos 29 | # search subfolders 30 | # command to refresh cache 31 | 32 | script_help = """Script: Valid commands are: 33 | INSTALL script 34 | SEARCH word 35 | EDIT script 36 | UPDATE script 37 | REMOVE script""" 38 | 39 | addon_dir = os.path.join(hexchat.get_info('configdir'), 'addons') 40 | addon_types = ('py', 'pl', 'lua', 'js') # tcl has no way to unload a single script? 41 | addon_cache = {} 42 | # User, Repo, Folder 43 | addon_sites = (('TingPing', 'plugins', 'HexChat'), 44 | ('Farow', 'hexchat-scripts', '')) 45 | 46 | 47 | def expand_script(script): 48 | return os.path.join(addon_dir, script) 49 | 50 | def build_url(site, type='', script=''): 51 | if type == 'raw': 52 | return 'https://raw.github.com/{1}/{2}/master/{3}/{0}'.format(script, *site) 53 | elif type == 'api': 54 | return 'https://api.github.com/repos/{0}/{1}/contents/{2}'.format(*site) 55 | else: 56 | return 'https://github.com/{1}/{2}/tree/master/{3}/{0}'.format(script, *site) 57 | 58 | 59 | def update_addons(): 60 | global addon_cache 61 | addon_cache = {} 62 | 63 | for site in addon_sites: 64 | try: 65 | response = urllib_request.urlopen(build_url(site, type='api')) 66 | text = response.read().decode('utf-8') 67 | response.close() 68 | data = json.loads(text) 69 | addon_cache[site] = [d['name'] for d in data if d['name'].split('.')[-1] in addon_types] 70 | except urllib_error.HTTPError as err: 71 | if err.code == 403: 72 | print("Script: You have hit Github's rate-limit") 73 | return 74 | except IOError: # No internet 75 | return 76 | 77 | print('Script: Addon cache updated.') 78 | 79 | def unload_cb(script_file): 80 | hexchat.command('unload {}'.format(script_file)) 81 | return False 82 | 83 | def load_cb(script_file): 84 | hexchat.command('load {}'.format(script_file)) 85 | return False 86 | 87 | def download(script, unload): 88 | for site in addon_cache.keys(): 89 | if script in addon_cache[site]: 90 | script_file = expand_script(script) 91 | print('Script: Downloading {}...'.format(script)) 92 | try: 93 | urllib_request.urlretrieve(build_url(site, type='raw', script=script), script_file) 94 | except urllib_error.HTTPError as err: 95 | print('Script: Error downloading {} ({})'.format(script, err)) 96 | else: 97 | print('Script: Download complete, {}...'.format('reloading' if unload else 'loading')) 98 | if unload: # Updated 99 | # Threading causes odd timing issues, using timer fixes it. 100 | hexchat.hook_timer(0, unload_cb, script_file) 101 | hexchat.hook_timer(0, load_cb, script_file) 102 | return 103 | 104 | print('Script: Could not find {}'.format(script)) 105 | 106 | 107 | def install(script, unload): 108 | if THREADS_SUPPORTED: 109 | threading.Thread(target=download, args=(script, unload)).start() 110 | else: 111 | download(script, unload) 112 | 113 | def search(word): 114 | matches = [(site, script) for site in addon_cache.keys() for script in addon_cache[site] if word in script] 115 | 116 | if not matches: 117 | print('Script: {} not found.'.format(word)) 118 | else: 119 | print('Script: {} matches found for {}.'.format(len(matches), word)) 120 | for (site, script) in matches: 121 | print('\t\t{}: \00318\037{}\017'.format(script, build_url(site, type='', script=script))) 122 | 123 | 124 | def script_cb(word, word_eol, userdata): 125 | if len(word) > 2: 126 | cmd = word[1].lower() 127 | arg = word[2] 128 | else: 129 | hexchat.command('help script') 130 | return hexchat.EAT_ALL 131 | 132 | if cmd == 'install': 133 | if os.path.exists(expand_script(arg)): 134 | print('Script: {} is already installed.'.format(arg)) 135 | return hexchat.EAT_ALL 136 | else: 137 | install(arg, False) 138 | elif cmd == 'update': 139 | if os.path.exists(expand_script(arg)): 140 | install(arg, True) 141 | elif cmd == 'edit': 142 | if sys.platform.startswith('linux'): 143 | # This will popup a choose program prompt /url doesn't 144 | hexchat.command('exec -d gvfs-open {}'.format(expand_script(arg))) 145 | else: 146 | hexchat.command('url {}'.format(expand_script(arg))) 147 | elif cmd == 'search': 148 | search(arg) 149 | elif cmd == 'remove': 150 | if arg == 'script.py': 151 | print('Script: I refuse.') 152 | return hexchat.EAT_ALL 153 | if os.path.exists(expand_script(arg)): 154 | hexchat.command('unload ' + expand_script(arg)) 155 | os.remove(expand_script(arg)) 156 | else: 157 | print('Script: {} is not installed.'.format(arg)) 158 | else: 159 | hexchat.command('help script') 160 | 161 | return hexchat.EAT_ALL 162 | 163 | def unload_callback(userdata): 164 | print(__module_name__, 'version', __module_version__, 'unloaded.') 165 | 166 | if THREADS_SUPPORTED: 167 | threading.Thread(target=update_addons).start() 168 | else: 169 | update_addons() 170 | hexchat.hook_command('script', script_cb, help=script_help) 171 | hexchat.hook_unload(unload_callback) 172 | print(__module_name__, 'version', __module_version__, 'loaded.') 173 | -------------------------------------------------------------------------------- /HexChat/serverpass.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('ServerPass', '1', 'Allows using server password with another login method') 3 | 4 | -- Configure networks here: 5 | -- The key must match a network name in your network list 6 | -- Set the value to an empty string to re-use the same network list password 7 | -- Otherwise set the value to any string for a custom password 8 | local NETWORKS = { 9 | ['freenode'] = '', 10 | -- ['freenode'] = 'password', 11 | } 12 | 13 | hexchat.hook_print('Connected', function (args) 14 | local network = hexchat.get_info('network') 15 | local username = hexchat.prefs['irc_user_name'] 16 | local password = NETWORKS[network] 17 | 18 | if password then 19 | if password == '' then 20 | password = hexchat.get_info('password') 21 | end 22 | 23 | hexchat.command(string.format('QUOTE PASS %s:%s', username, password)) 24 | end 25 | end) 26 | -------------------------------------------------------------------------------- /HexChat/session.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import hexchat 3 | 4 | __module_name__ = "session" 5 | __module_author__ = "TingPing" 6 | __module_version__ = "1" 7 | __module_description__ = "Saves current session for next start" 8 | # To use just disable auto-connect and start using 'Quit and Save' from the menu. 9 | 10 | def load_session(): 11 | for pref in hexchat.list_pluginpref(): 12 | if len(pref) > 8 and pref[:8] == 'session_': 13 | network = pref[8:] 14 | channels = hexchat.get_pluginpref('session_' + network).split(',') 15 | hexchat.command('url irc://"{}"/'.format(network)) # Using url avoids autojoin 16 | hexchat.find_context(server=network).set() 17 | delay = hexchat.get_prefs('irc_join_delay') + 10 18 | for chan in channels: 19 | if chan[0] != '#': 20 | hexchat.command('timer {} query -nofocus {}'.format(delay, chan)) 21 | else: 22 | hexchat.command('timer {} join {}'.format(delay, chan)) 23 | 24 | hexchat.del_pluginpref('session_' + network) 25 | 26 | def quit_cb(word, word_eol, userdata): 27 | networks = {} 28 | 29 | for chan in hexchat.get_list('channels'): 30 | if chan.type == 2 or chan.type == 3: # Ignore notices and server tabs 31 | if not chan.network in networks: 32 | networks[chan.network] = [] 33 | if (chan.channelkey): 34 | networks[chan.network].append(chan.channel + ' ' + chan.channelkey) 35 | else: 36 | networks[chan.network].append(chan.channel) 37 | 38 | for network, channels in networks.items(): 39 | hexchat.set_pluginpref('session_' + network, ','.join(channels)) 40 | hexchat.find_context(server=network).command('quit') 41 | 42 | hexchat.command('timer 1 killall') 43 | return hexchat.EAT_ALL 44 | 45 | def unload_cb(userdata): 46 | print(__module_name__, 'version', __module_version__, 'unloaded.') 47 | 48 | load_session() 49 | hexchat.hook_command('quitandsave', quit_cb) 50 | hexchat.hook_unload(unload_cb) 51 | hexchat.command('menu -p-1 add "HexChat/Quit and Save" "quitandsave"') 52 | print(__module_name__, 'version', __module_version__, 'loaded.') 53 | -------------------------------------------------------------------------------- /HexChat/smartparts.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('SmartParts', '1', 'Intelligently hide parts, joins, modes, and nick changes') 3 | 4 | local TIME_THRESHOLD = 60 * 5 -- in seconds 5 | 6 | local function check_lasttalk (nick) 7 | local nick = hexchat.strip(nick) 8 | for user in hexchat.iterate('users') do 9 | if hexchat.nickcmp(nick, user.nick) == 0 then 10 | if os.time() - user.lasttalk > TIME_THRESHOLD then 11 | return hexchat.EAT_HEXCHAT 12 | else 13 | return hexchat.EAT_NONE 14 | end 15 | end 16 | end 17 | 18 | return hexchat.EAT_HEXCHAT 19 | end 20 | 21 | local function check_you (nick) 22 | return hexchat.nickcmp(hexchat.get_info('nick'), nick) == 0 23 | end 24 | 25 | local function check_notify (nick) 26 | local nick = hexchat.strip(nick) 27 | for user in hexchat.iterate('notify') do 28 | if user.nick == nick then 29 | return true 30 | end 31 | end 32 | 33 | return false 34 | end 35 | 36 | 37 | hexchat.hook_print('Join', function (args) 38 | if check_notify (args[1]) then 39 | return hexchat.EAT_NONE 40 | else 41 | return hexchat.EAT_HEXCHAT 42 | end 43 | end, hexchat.PRI_LOW) 44 | 45 | hexchat.hook_print('Change Nick', function (args) 46 | if check_notify(args[1]) or check_notify(args[2]) then 47 | return hexchat.EAT_NONE 48 | end 49 | 50 | return check_lasttalk(args[1]) 51 | end, hexchat.PRI_LOW) 52 | 53 | hexchat.hook_print('Raw Modes', function (args) 54 | if check_you(args[1]) or check_notify(args[1]) then 55 | return hexchat.EAT_NONE 56 | end 57 | 58 | -- TODO: Parse targets 59 | 60 | return check_lasttalk(args[1]) 61 | end, hexchat.PRI_LOW) 62 | 63 | for _, event in pairs({'Quit', 'Part', 'Part with Reason'}) do 64 | hexchat.hook_print(event, function (args) 65 | if check_notify(args[1]) then 66 | return hexchat.EAT_NONE 67 | end 68 | 69 | return check_lasttalk(args[1]) 70 | end, hexchat.PRI_LOW) 71 | end 72 | 73 | for _, event in pairs({'Channel Operator', 'Channel Voices'}) do 74 | hexchat.hook_print(event, function (args) 75 | if check_you(args[1]) or check_you(args[2]) or check_notify(args[2]) then 76 | return hexchat.EAT_NONE 77 | end 78 | 79 | return check_lasttalk(args[2]) 80 | end, hexchat.PRI_LOW) 81 | end 82 | -------------------------------------------------------------------------------- /HexChat/statuscolor.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('StatusColor', '1', 'Color nicks based upon user modes') 3 | 4 | -- TODO: Update these to reflect default theme 5 | local MODES = { 6 | ['+'] = '24', 7 | ['%'] = '28', 8 | ['@'] = '19', 9 | ['&'] = '21', 10 | ['~'] = '22', 11 | } 12 | 13 | local edited = false 14 | local function on_message (args, attrs, event) 15 | if edited then 16 | return hexchat.EAT_NONE 17 | end 18 | 19 | local color = MODES[args[3]] 20 | if not color then 21 | return hexchat.EAT_NONE 22 | end 23 | 24 | -- In > 2.12.3 we need to be explicit about color changes 25 | if event:sub(-7, -1) == 'Hilight' then 26 | hexchat.command('gui color 3') 27 | elseif event:sub(1, 4) ~= 'Your' then 28 | hexchat.command('gui color 2') 29 | end 30 | 31 | edited = true 32 | args[1] = '\003' .. color .. hexchat.strip(args[1]) .. '\00399' 33 | args[3] = '\003' .. color .. args[3] .. '\00399' 34 | hexchat.emit_print_attrs(attrs, event, unpack(args)) 35 | edited = false 36 | 37 | return hexchat.EAT_ALL 38 | end 39 | 40 | for _, event in pairs({ 41 | 'Channel Message', 42 | 'Channel Action', 43 | 'Channel Msg Hilight', 44 | 'Channel Action Hilight', 45 | 'Your Message', 46 | 'Your Action' 47 | }) do 48 | hexchat.hook_print_attrs(event, function (args, attrs) 49 | return on_message(args, attrs, event) 50 | end, hexchat.PRI_LOW) 51 | end 52 | -------------------------------------------------------------------------------- /HexChat/twitch.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('Twitch', '1', 'Better integration with twitch.tv') 3 | 4 | local function is_twitch () 5 | local server = hexchat.get_info('server') 6 | if hexchat.nickcmp(hexchat.get_info('network'), 'Twitch') == 0 then 7 | return true 8 | elseif server and server:find('twitch.tv$') then 9 | return true 10 | else 11 | return false 12 | end 13 | end 14 | 15 | 16 | -- Commands from http://help.twitch.tv/customer/portal/articles/659095-chat-moderation-commands 17 | -- /ban may conflict with other scripts nothing we can do about that 18 | -- /clear is an existing command, just override it 19 | for _, command in pairs({ 20 | 'timeout', 'slow', 'slowoff', 'subscribers', 'subscribersoff', 21 | 'mod', 'unmod', 'mods', 'clear', 'ban', 'unban', 'commercial', 22 | 'r9kbeta', 'r9kbetaoff', 'color', 'host', 23 | }) do 24 | hexchat.hook_command(command, function (word, word_eol) 25 | if not is_twitch() then return end 26 | 27 | hexchat.command('say .' .. word_eol[1]) 28 | return hexchat.EAT_ALL 29 | end) 30 | end 31 | 32 | for command, alias in pairs({['op'] = 'mod', ['deop'] = 'unmod'}) do 33 | hexchat.hook_command(command, function (word, word_eol) 34 | if not is_twitch() then return end 35 | 36 | if word[2] then 37 | hexchat.command(string.format('say .%s %s', alias, word_eol[2])) 38 | else 39 | hexchat.command('say .' .. alias) 40 | end 41 | return hexchat.EAT_ALL 42 | end) 43 | end 44 | 45 | hexchat.hook_server('PRIVMSG', function (word, word_eol) 46 | if not is_twitch() then return end 47 | 48 | -- Move jtv messages to the server tab 49 | if word[1]:find('^:jtv!') and word[3]:sub(1, 1) ~= '#' then 50 | local id = hexchat.prefs['id'] 51 | for chan in hexchat.iterate('channels') do 52 | if chan.type == 1 and chan.id == id then 53 | chan.context:emit_print('Server Text', word_eol[4]:sub(2)) 54 | return hexchat.EAT_ALL 55 | end 56 | end 57 | end 58 | end) 59 | 60 | hexchat.hook_server('421', function (word, word_eol) 61 | if not is_twitch() then return end 62 | 63 | -- Ignore unknown command errors 64 | if word[4] == 'WHO' or word[4] == 'WHOIS' then 65 | return hexchat.EAT_ALL 66 | end 67 | end) 68 | 69 | hexchat.hook_print('Your Message', function (args) 70 | if not is_twitch() then return end 71 | 72 | -- Eat any message starting with a '.', twitch eats all of them too 73 | if args[2]:sub(1, 1) == '.' then 74 | return hexchat.EAT_ALL 75 | end 76 | end) 77 | 78 | hexchat.hook_print('Channel Message', function (args) 79 | if not is_twitch() then return end 80 | 81 | -- Format messages from twitchnotify 82 | if args[1] == 'twitchnotify' then 83 | print('\00314*\t' .. args[2]) 84 | return hexchat.EAT_HEXCHAT 85 | end 86 | end, hexchat.PRI_LOW) 87 | -------------------------------------------------------------------------------- /HexChat/undo.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | local bit = bit 3 | if not bit then 4 | bit = require('bit32') 5 | end 6 | local lgi = require('lgi') 7 | local Gdk = lgi.require('Gdk', '2.0') 8 | 9 | hexchat.register('Undo', '1', 'Add keybindings to undo/redo') 10 | 11 | local MAX_LEVELS = 10 12 | 13 | local undobufs = {} 14 | local redobufs = {} 15 | 16 | 17 | local stack = {} 18 | 19 | function stack:new () 20 | local new_stack = {} 21 | setmetatable(new_stack, {__index=stack}) 22 | return new_stack 23 | end 24 | 25 | function stack:push (val) 26 | if #self == MAX_LEVELS then 27 | table.remove(self, 1) 28 | end 29 | 30 | self[#self + 1] = val 31 | end 32 | 33 | function stack:pop () 34 | local val = self[#self] 35 | self[#self] = nil 36 | return val 37 | end 38 | 39 | 40 | local function get_valid_mod (modifier) 41 | -- We only care about a few modifiers 42 | local mods = 0 43 | for _, mod in pairs({ 44 | Gdk.ModifierType.CONTROL_MASK, 45 | Gdk.ModifierType.SHIFT_MASK, 46 | Gdk.ModifierType.MOD1_MASK, -- alt 47 | }) do 48 | mods = bit.bor(mods, mod) 49 | end 50 | 51 | return bit.band(tonumber(modifier), mods) 52 | end 53 | 54 | hexchat.hook_print('Key Press', function (args) 55 | local network = hexchat.get_info('network') 56 | if network == nil then 57 | network = '' 58 | end 59 | local bufname = hexchat.get_info('channel') .. '_' .. network 60 | local key = tonumber(args[1]) 61 | local mod = get_valid_mod(args[2]) 62 | local input = hexchat.get_info('inputbox') 63 | 64 | local undostack = undobufs[bufname] or stack:new() 65 | local redostack = redobufs[bufname] or stack:new() 66 | if #undostack == 0 and #redostack == 0 then 67 | undobufs[bufname] = undostack 68 | redobufs[bufname] = redostack 69 | end 70 | 71 | if key == Gdk.KEY_z and mod == Gdk.ModifierType.CONTROL_MASK then 72 | local text = undostack:pop() 73 | if text then 74 | if text == input then 75 | redostack:push(text) 76 | text = undostack:pop() or '' 77 | end 78 | hexchat.command('settext ' .. text) 79 | hexchat.command('setcursor ' .. tostring(#text)) 80 | redostack:push(text) 81 | end 82 | elseif (key == Gdk.KEY_y and mod == Gdk.ModifierType.CONTROL_MASK) or 83 | (key == Gdk.KEY_Z and mod == bit.bor(Gdk.ModifierType.CONTROL_MASK, Gdk.ModifierType.SHIFT_MASK)) then 84 | local text = redostack:pop() 85 | if text then 86 | if text == input then 87 | text = redostack:pop() or '' 88 | end 89 | hexchat.command('settext ' .. text) 90 | hexchat.command('setcursor ' .. tostring(#text)) 91 | end 92 | else -- Any key press 93 | if undostack[#undostack] ~= input then 94 | undostack:push(input) 95 | end 96 | end 97 | end) 98 | -------------------------------------------------------------------------------- /HexChat/undoclose.py: -------------------------------------------------------------------------------- 1 | from sys import platform 2 | from collections import deque 3 | import hexchat 4 | 5 | __module_name__ = "Undo Close" 6 | __module_version__ = "0.1" 7 | __module_description__ = "Adds keybinding to undo close tab" 8 | __module_author__ = "TingPing" 9 | 10 | if platform == 'darwin': 11 | primarymod = 1 << 28 12 | else: 13 | primarymod = 1 << 2 14 | shiftmod = 1 << 0 15 | 16 | shiftctrlmod = primarymod|shiftmod 17 | 18 | close_history = deque(maxlen=30) 19 | 20 | def get_valid_mod(mod): 21 | """Modifiers are full of junk we dont care about, remove them""" 22 | return int(mod) & (1 << 0 | 1 << 2 | 1 << 3 | 1 << 28) 23 | 24 | def contextclosed_cb(word, word_eol, userdata): 25 | global close_history 26 | 27 | ctx = hexchat.get_context() 28 | for chan in hexchat.get_list('channels'): 29 | if chan.context == ctx: 30 | if chan.type != 2: # Only want channels 31 | return 32 | 33 | net = hexchat.get_info('network') 34 | if not net: 35 | net = hexchat.get_info('server') 36 | if not net: 37 | return 38 | 39 | chan = hexchat.get_info('channel') 40 | if not chan: 41 | return 42 | 43 | close_history.append('irc://{}/{}'.format(net, chan)) 44 | 45 | def keypress_cb(word, word_eol, userdata): 46 | global close_history 47 | 48 | key, mod = word[0], get_valid_mod(word[1]) 49 | 50 | if (key, mod) == ('84', shiftctrlmod): # Ctrl+Shift+t 51 | try: 52 | last = close_history.pop() 53 | hexchat.command('url {}'.format(last)) 54 | except IndexError: 55 | pass 56 | 57 | hexchat.hook_print("Close Context", contextclosed_cb) 58 | hexchat.hook_print('Key Press', keypress_cb) 59 | -------------------------------------------------------------------------------- /HexChat/wordhl.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('WordHL', '1', 'Highlights some words of importance') 3 | -- When you want to notice something, but not really get 'highlighted' 4 | 5 | local function nocase (s) 6 | s = string.gsub(s, "%a", function (c) 7 | return string.format("[%s%s]", string.lower(c), 8 | string.upper(c)) 9 | end) 10 | return '(' .. s .. ')' 11 | end 12 | 13 | local HLWORDS = { 14 | nocase('hexchat'), 15 | } -- case insensitive 16 | local COLOR = '20' -- Red 17 | 18 | local function replace_x(s) 19 | return '\003' .. COLOR .. s .. '\00399' 20 | end 21 | 22 | local event_edited = false 23 | local function on_message (args, event_type) 24 | if event_edited then 25 | return -- Ignore own events 26 | end 27 | 28 | local message = args[2] 29 | for _, word in ipairs(HLWORDS) do 30 | message = message:gsub("%f[%w_]" .. word .. "%f[^%w_]", replace_x) 31 | end 32 | 33 | if message ~= args[2] then 34 | event_edited = true 35 | args[2] = message 36 | hexchat.emit_print(event_type, unpack(args)) 37 | event_edited = false 38 | 39 | hexchat.command('gui color 3') 40 | return hexchat.EAT_ALL 41 | end 42 | 43 | end 44 | 45 | for _, event in ipairs({'Channel Action', 'Channel Message'}) do 46 | hexchat.hook_print(event, function (args) 47 | return on_message (args, event) 48 | end, hexchat.PRI_HIGH) 49 | end 50 | -------------------------------------------------------------------------------- /HexChat/zncbuffer.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | hexchat.register('ZNC Buffers', '1', 'Add menu options to manage ZNC buffers') 3 | 4 | -- Add menus 5 | hexchat.command('menu -p4 add "$TAB/ZNC"') 6 | hexchat.command('menu add "$TAB/ZNC/Clear Buffer" ".zncclearbuffer %s"') 7 | hexchat.command('menu add "$TAB/ZNC/Play Buffer" "znc playbuffer %s"') 8 | hexchat.hook_unload(function () hexchat.command('menu del "$TAB/ZNC') end) 9 | 10 | 11 | -- Ignore our own actions 12 | local recently_cleared = {} 13 | 14 | hexchat.hook_command('.zncclearbuffer', function(word, word_eol) 15 | local name = word[2] 16 | 17 | -- Ignore znc queries 18 | if name:sub(1, 1) ~= '*' then 19 | recently_cleared[name] = true 20 | hexchat.command('znc clearbuffer ' .. name) 21 | end 22 | 23 | return hexchat.EAT_ALL 24 | end) 25 | 26 | hexchat.hook_server('PRIVMSG', function(word, word_eol) 27 | local cleared_channel = word_eol[1]:match('^:%*status!znc@znc.in [^:]+:%[%d+] buffers matching %[([^%]]+)] have been cleared$') 28 | 29 | if cleared_channel and recently_cleared[cleared_channel] then 30 | recently_cleared[cleared_channel] = nil 31 | return hexchat.EAT_ALL 32 | end 33 | end) 34 | 35 | hexchat.hook_command('zncclosepm', function (word, word_eol) 36 | local id = hexchat.props.id 37 | 38 | for chan in hexchat.iterate('channels') do 39 | if chan.id == id and chan.type == 3 then 40 | hexchat.command('.zncclearbuffer ' .. chan.channel) 41 | chan.context:command('close') 42 | end 43 | end 44 | 45 | return hexchat.EAT_ALL 46 | end) 47 | -------------------------------------------------------------------------------- /Limnoria/Markov/README.txt: -------------------------------------------------------------------------------- 1 | This plugin watches everything that is said in the channel and stores 2 | word pairs to a database. On request (or at random intervals if 3 | configured to do so), the bot will then randomly string together the 4 | word pairs to form some semblance of a sentence. 5 | -------------------------------------------------------------------------------- /Limnoria/Markov/__init__.py: -------------------------------------------------------------------------------- 1 | ### 2 | # Copyright (c) 2005, James Vega 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # 8 | # * Redistributions of source code must retain the above copyright notice, 9 | # this list of conditions, and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions, and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the author of this software nor the name of 14 | # contributors to this software may be used to endorse or promote products 15 | # derived from this software without specific prior written consent. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | ### 29 | 30 | """ 31 | Silently listens to a channel, building a database of Markov Chains for later 32 | hijinks. To read more about Markov Chains, check out 33 | . When the database is 34 | large enough, you can have it make fun little random messages from it. 35 | """ 36 | 37 | from imp import reload 38 | 39 | import supybot 40 | import supybot.world as world 41 | 42 | # Use this for the version of this plugin. You may wish to put a CVS keyword 43 | # in here if you're keeping the plugin in CVS or some similar system. 44 | __version__ = "0.3" 45 | 46 | __author__ = supybot.authors.jamessan 47 | 48 | # This is a dictionary mapping supybot.Author instances to lists of 49 | # contributions. 50 | __contributors__ = {} 51 | 52 | from . import config 53 | from . import plugin 54 | reload(plugin) # In case we're being reloaded. 55 | # Add more reloads here if you add third-party modules and want them to be 56 | # reloaded when this plugin is reloaded. Don't forget to import them as well! 57 | 58 | if world.testing: 59 | from . import test 60 | 61 | Class = plugin.Class 62 | configure = config.configure 63 | 64 | 65 | # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 66 | -------------------------------------------------------------------------------- /Limnoria/Markov/config.py: -------------------------------------------------------------------------------- 1 | ### 2 | # Copyright (c) 2005,2008, James Vega 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # 8 | # * Redistributions of source code must retain the above copyright notice, 9 | # this list of conditions, and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions, and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the author of this software nor the name of 14 | # contributors to this software may be used to endorse or promote products 15 | # derived from this software without specific prior written consent. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | ### 29 | 30 | import supybot.conf as conf 31 | import supybot.registry as registry 32 | 33 | def configure(advanced): 34 | # This will be called by supybot to configure this module. advanced is 35 | # a bool that specifies whether the user identified himself as an advanced 36 | # user or not. You should effect your configuration by manipulating the 37 | # registry as appropriate. 38 | from supybot.questions import expect, anything, something, yn 39 | conf.registerPlugin('Markov', True) 40 | 41 | 42 | Markov = conf.registerPlugin('Markov') 43 | conf.registerPlugin('Markov') 44 | conf.registerChannelValue(Markov, 'ignoreBotCommands', 45 | registry.Boolean(True, """Determines whether messages addressed to the 46 | bot are ignored.""")) 47 | conf.registerChannelValue(Markov, 'minChainLength', 48 | registry.PositiveInteger(1, """Determines the length of the smallest chain 49 | which the markov command will generate.""")) 50 | conf.registerChannelValue(Markov, 'maxAttempts', 51 | registry.PositiveInteger(1, """Determines the maximum number of times the 52 | bot will attempt to generate a chain that meets or exceeds the size set in 53 | minChainLength.""")) 54 | 55 | conf.registerGroup(Markov, 'randomSpeaking') 56 | conf.registerChannelValue(Markov.randomSpeaking, 57 | 'probability', registry.Probability(0, """Determines the probability that 58 | will be checked against to determine whether the bot should randomly say 59 | something. If 0, the bot will never say anything on it's own. If 1, the 60 | bot will speak every time we make a check.""")) 61 | conf.registerChannelValue(Markov.randomSpeaking, 62 | 'maxDelay', registry.PositiveInteger(10, """Determines the upper bound for 63 | how long the bot will wait before randomly speaking. The delay is a 64 | randomly generated number of seconds below the value of this config 65 | variable.""")) 66 | conf.registerChannelValue(Markov.randomSpeaking, 67 | 'throttleTime', registry.PositiveInteger(300, """Determines the minimum 68 | number of seconds between the bot randomly speaking.""")) 69 | 70 | 71 | # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 72 | -------------------------------------------------------------------------------- /Limnoria/Markov/plugin.py: -------------------------------------------------------------------------------- 1 | ### 2 | # Copyright (c) 2005,2008, James Vega 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # 8 | # * Redistributions of source code must retain the above copyright notice, 9 | # this list of conditions, and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions, and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the author of this software nor the name of 14 | # contributors to this software may be used to endorse or promote products 15 | # derived from this software without specific prior written consent. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | ### 29 | 30 | import time 31 | import queue as Queue 32 | import random 33 | import threading 34 | 35 | import supybot.utils as utils 36 | import supybot.world as world 37 | from supybot.commands import * 38 | import supybot.ircmsgs as ircmsgs 39 | import supybot.plugins as plugins 40 | import supybot.ircutils as ircutils 41 | import supybot.schedule as schedule 42 | import supybot.callbacks as callbacks 43 | 44 | class SqlAlchemyMarkovDB(object): 45 | def __init__(self, filename, engine): 46 | self.dbs = ircutils.IrcDict() 47 | self.filename = filename 48 | self.engine = engine 49 | 50 | def close(self): 51 | self.dbs.clear() 52 | 53 | def _getDb(self, channel, debug=False): 54 | if channel in self.dbs: 55 | return self.dbs[channel] 56 | 57 | try: 58 | import sqlalchemy as sql 59 | self.sql = sql 60 | except ImportError: 61 | raise callbacks.Error('You need to have SQLAlchemy installed to use this ' \ 62 | 'plugin. Download it at ') 63 | 64 | filename = plugins.makeChannelFilename(self.filename, channel) 65 | engine = sql.create_engine(self.engine + filename, echo=debug) 66 | metadata = sql.MetaData() 67 | firsts = sql.Table('firsts', metadata, 68 | sql.Column('id', sql.Integer, primary_key=True), 69 | sql.Column('first', sql.Text, unique=True), 70 | sql.Column('count', sql.Integer, default=1), 71 | ) 72 | lasts = sql.Table('lasts', metadata, 73 | sql.Column('id', sql.Integer, primary_key=True), 74 | sql.Column('last', sql.Text, unique=True), 75 | sql.Column('count', sql.Integer, default=1), 76 | ) 77 | pairs = sql.Table('pairs', metadata, 78 | sql.Column('id', sql.Integer, primary_key=True), 79 | sql.Column('first', sql.Text, default=sql.null), 80 | sql.Column('second', sql.Text, default=sql.null), 81 | sql.Column('follow', sql.Text, default=sql.null), 82 | sql.Column('count', sql.Integer, default=1), 83 | sql.UniqueConstraint('first', 'second', 'follow'), 84 | ) 85 | metadata.create_all(engine) 86 | self.dbs[channel] = (engine, firsts, lasts, pairs) 87 | return self.dbs[channel] 88 | 89 | def _addFirst(self, db, table, first): 90 | s = self.sql.select([table.c.count], table.c.first==first) 91 | results = db.execute(s) 92 | r = results.fetchone() 93 | if r is None: 94 | db.execute(table.insert(), first=first).close() 95 | else: 96 | db.execute(table.update(), count=r[0]+1).close() 97 | 98 | def _addLast(self, db, table, last): 99 | s = self.sql.select([table.c.count], table.c.last==last) 100 | results = db.execute(s) 101 | r = results.fetchone() 102 | if r is None: 103 | db.execute(table.insert(), last=last).close() 104 | else: 105 | db.execute(table.update(), count=r[0]+1).close() 106 | 107 | def addPair(self, channel, first, second, follower, isFirst, isLast): 108 | (db, firsts, lasts, pairs) = self._getDb(channel) 109 | if isFirst: 110 | self._addFirst(db, firsts, follower) 111 | return 112 | if isLast: 113 | self._addLast(db, lasts, second) 114 | s = self.sql.select([pairs.c.count], 115 | self.sql.and_(pairs.c.first==first, 116 | pairs.c.second==second, 117 | pairs.c.follow==follower)) 118 | results = db.execute(s) 119 | r = results.fetchone() 120 | if r is None: 121 | db.execute(pairs.insert(), first=first, second=second, 122 | follow=follower).close() 123 | else: 124 | db.execute(pairs.update(), count=r[0]+1).close() 125 | 126 | def _weightedChoice(self, results): 127 | L = [] 128 | for t in results: 129 | c = t[-1] 130 | while c > 0: 131 | c -= 1 132 | L.append(t[:-1]) 133 | return utils.iter.choice(L) 134 | 135 | def getFirstPair(self, channel): 136 | (db, _, _, pairs) = self._getDb(channel) 137 | s = self.sql.select([pairs.c.first, pairs.c.second, pairs.c.count], 138 | pairs.c.first==None) 139 | results = db.execute(s) 140 | r = results.fetchall() 141 | results.close() 142 | if not r: 143 | raise KeyError 144 | return self._weightedChoice(r) 145 | 146 | def getFollower(self, channel, first, second): 147 | (db, _, _, pairs) = self._getDb(channel) 148 | s = self.sql.select([pairs.c.first, pairs.c.second, 149 | pairs.c.follow, pairs.c.count], 150 | self.sql.and_(pairs.c.first==first, 151 | pairs.c.second==second)) 152 | results = db.execute(s) 153 | r = results.fetchall() 154 | results.close() 155 | if not r: 156 | raise KeyError 157 | print('foo') 158 | print((repr(r))) 159 | L = self._weightedChoice(r) 160 | isLast = False 161 | if not L[-1]: 162 | isLast = True 163 | return (L[-2], isLast) 164 | 165 | def firsts(self, channel): 166 | (db, firsts, _, _) = self._getDb(channel) 167 | s = self.sql.select([firsts.c.count]) 168 | results = db.execute(s) 169 | r = results.fetchall() 170 | results.close() 171 | if not r: 172 | return 0 173 | else: 174 | return sum([x[0] for x in r]) 175 | 176 | def lasts(self, channel): 177 | (db, _, lasts, _) = self._getDb(channel) 178 | s = self.sql.select([lasts.c.count]) 179 | results = db.execute(s) 180 | r = results.fetchall() 181 | results.close() 182 | if not r: 183 | return 0 184 | else: 185 | return sum([x[0] for x in r]) 186 | 187 | def pairs(self, channel): 188 | (db, _, _, pairs) = self._getDb(channel) 189 | s = self.sql.select([pairs.c.count]) 190 | results = db.execute(s) 191 | r = results.fetchall() 192 | results.close() 193 | if not r: 194 | return 0 195 | else: 196 | return sum([x[0] for x in r]) 197 | 198 | def follows(self, channel): 199 | (db, _, _, pairs) = self._getDb(channel) 200 | s = self.sql.select([pairs.c.count], 201 | self.sql.not_(pairs.c.follow==None)) 202 | results = db.execute(s) 203 | r = results.fetchall() 204 | results.close() 205 | if not r: 206 | return 0 207 | else: 208 | return sum([x[0] for x in r]) 209 | 210 | class DbmMarkovDB(object): 211 | def __init__(self, filename): 212 | self.dbs = ircutils.IrcDict() 213 | self.filename = filename 214 | 215 | def close(self): 216 | for db in self.dbs.values(): 217 | db.close() 218 | 219 | def _getDb(self, channel): 220 | import dbm 221 | if channel not in self.dbs: 222 | filename = plugins.makeChannelFilename(self.filename, channel) 223 | # To keep the code simpler for addPair, I decided not to make 224 | # self.dbs[channel]['firsts'] and ['lasts']. Instead, we'll pad 225 | # the words list being sent to addPair such that ['\n \n'] will be 226 | # ['firsts'] and ['\n'] will be ['lasts']. 227 | self.dbs[channel] = dbm.open(filename, 'c') 228 | return self.dbs[channel] 229 | 230 | def _flush(self, db): 231 | if hasattr(db, 'sync'): 232 | db.sync() 233 | if hasattr(db, 'flush'): 234 | db.flush() 235 | 236 | def _addPair(self, channel, pair, follow): 237 | db = self._getDb(channel) 238 | # EW! but necessary since not all dbm backends support 239 | # "combined in db" 240 | if pair.encode('utf-8') in db: 241 | db[pair] = b' '.join([db[pair], follow.encode('utf-8')]) 242 | else: 243 | db[pair] = follow.encode('utf-8') 244 | self._flush(db) 245 | 246 | def _combine(self, first, second): 247 | first = first or '\n' 248 | second = second or '\n' 249 | return '%s %s' % (first, second) 250 | 251 | def addPair(self, channel, first, second, follower, isFirst, isLast): 252 | combined = self._combine(first, second) 253 | self._addPair(channel, combined, follower or '\n') 254 | if isLast: 255 | self._addPair(channel, '\n', second) 256 | 257 | def getFirstPair(self, channel): 258 | db = self._getDb(channel) 259 | firsts = db['\n \n'].split() 260 | if firsts: 261 | return (None, utils.iter.choice(firsts)) 262 | else: 263 | raise KeyError('No firsts for %s.' % channel) 264 | 265 | def getFollower(self, channel, first, second): 266 | db = self._getDb(channel) 267 | followers = db[self._combine(first, second)] 268 | follower = utils.iter.choice(followers.split(' ')) 269 | last = False 270 | if follower == '\n': 271 | follower = None 272 | last = True 273 | return (follower, last) 274 | 275 | def firsts(self, channel): 276 | db = self._getDb(channel) 277 | if b'\n \n' in db: 278 | return len(set(db['\n \n'].split())) 279 | else: 280 | return 0 281 | 282 | def lasts(self, channel): 283 | db = self._getDb(channel) 284 | if b'\n' in db: 285 | return len(set(db['\n'].split())) 286 | else: 287 | return 0 288 | 289 | def pairs(self, channel): 290 | db = self._getDb(channel) 291 | pairs = [k for k in db.keys() if b'\n' not in k] 292 | return len(pairs) 293 | 294 | def follows(self, channel): 295 | db = self._getDb(channel) 296 | # dbm sucks in that we're not guaranteed to have .iteritems() 297 | # *cough*gdbm*cough*, so this has to be done the stupid way 298 | follows = [len([f for f in db[k].split() if f != b'\n']) 299 | for k in db.keys() if b'\n' not in k] 300 | return sum(follows) 301 | 302 | MarkovDB = plugins.DB('Markov', {'dbm': DbmMarkovDB, 303 | 'sqlalchemy': SqlAlchemyMarkovDB}) 304 | 305 | class MarkovWorkQueue(threading.Thread): 306 | def __init__(self, *args, **kwargs): 307 | name = 'Thread #%s (MarkovWorkQueue)' % world.threadsSpawned 308 | world.threadsSpawned += 1 309 | threading.Thread.__init__(self, name=name) 310 | self.db = MarkovDB(*args, **kwargs) 311 | self.q = Queue.Queue() 312 | self.killed = False 313 | self.setDaemon(True) 314 | self.start() 315 | 316 | def die(self): 317 | self.killed = True 318 | self.q.put(None) 319 | 320 | def enqueue(self, f): 321 | self.q.put(f) 322 | 323 | def run(self): 324 | while not self.killed: 325 | f = self.q.get() 326 | if f is not None: 327 | f(self.db) 328 | self.db.close() 329 | 330 | class Markov(callbacks.Plugin): 331 | def __init__(self, irc): 332 | self.q = MarkovWorkQueue() 333 | self.__parent = super(Markov, self) 334 | self.__parent.__init__(irc) 335 | self.lastSpoke = time.time() 336 | 337 | def die(self): 338 | self.q.die() 339 | self.__parent.die() 340 | 341 | def tokenize(self, m): 342 | if ircmsgs.isAction(m): 343 | return ircmsgs.unAction(m).split() 344 | elif ircmsgs.isCtcp(m): 345 | return [] 346 | else: 347 | return m.args[1].split() 348 | 349 | def doPrivmsg(self, irc, msg): 350 | if irc.isChannel(msg.args[0]): 351 | speakChan = msg.args[0] 352 | dbChan = plugins.getChannel(speakChan) 353 | canSpeak = False 354 | now = time.time() 355 | throttle = self.registryValue('randomSpeaking.throttleTime', 356 | speakChan) 357 | prob = self.registryValue('randomSpeaking.probability', speakChan) 358 | delay = self.registryValue('randomSpeaking.maxDelay', speakChan) 359 | if now > self.lastSpoke + throttle: 360 | canSpeak = True 361 | if canSpeak and random.random() < prob: 362 | f = self._markov(speakChan, irc, prefixNick=False, 363 | to=speakChan, Random=True) 364 | schedule.addEvent(lambda: self.q.enqueue(f), now + delay) 365 | self.lastSpoke = now + delay 366 | words = self.tokenize(msg) 367 | # This shouldn't happen often (CTCP messages being the possible 368 | # exception) 369 | if not words: 370 | return 371 | if self.registryValue('ignoreBotCommands', speakChan) and \ 372 | callbacks.addressed(irc.nick, msg): 373 | return 374 | words.insert(0, None) 375 | words.insert(0, None) 376 | words.append(None) 377 | def doPrivmsg(db): 378 | for (first, second, follower) in utils.seq.window(words, 3): 379 | db.addPair(dbChan, first, second, follower, 380 | isFirst=(first is None and second is None), 381 | isLast=(follower is None)) 382 | self.q.enqueue(doPrivmsg) 383 | 384 | def _markov(self, channel, irc, word1=None, word2=None, **kwargs): 385 | def f(db): 386 | minLength = self.registryValue('minChainLength', channel) 387 | maxTries = self.registryValue('maxAttempts', channel) 388 | Random = kwargs.pop('Random', None) 389 | while maxTries > 0: 390 | maxTries -= 1 391 | if word1 and word2: 392 | words = [word1, word2] 393 | resp = [word1] 394 | follower = word2 395 | elif word1 or word2: 396 | words = [None, word1 or word2] 397 | resp = [] 398 | follower = words[-1] 399 | else: 400 | try: 401 | # words is of the form [None, word] 402 | words = list(db.getFirstPair(channel)) 403 | resp = [] 404 | follower = words[-1] 405 | except KeyError: 406 | irc.error( 407 | format('I don\'t have any first pairs for %s.', 408 | channel)) 409 | return # We can't use raise here because the exception 410 | # isn't caught and therefore isn't sent to the 411 | # server 412 | last = False 413 | while not last: 414 | resp.append(follower) 415 | try: 416 | (follower, last) = db.getFollower(channel, words[-2], 417 | words[-1]) 418 | except KeyError: 419 | irc.error('I found a broken link in the Markov chain. ' 420 | ' Maybe I received two bad links to start ' 421 | 'the chain.') 422 | return # ditto here re: Raise 423 | words.append(follower) 424 | if len(resp) >= minLength: 425 | irc.reply(' '.join(resp), **kwargs) 426 | return 427 | else: 428 | continue 429 | if not Random: 430 | irc.error( 431 | format('I was unable to generate a Markov chain at least ' 432 | '%n long.', (minLength, 'word'))) 433 | else: 434 | self.log.debug('Not randomSpeaking. Unable to generate a ' 435 | 'Markov chain at least %n long.', 436 | (minLength, 'word')) 437 | return f 438 | 439 | def markov(self, irc, msg, args, channel, word1, word2): 440 | """[] [word1 [word2]] 441 | 442 | Returns a randomly-generated Markov Chain generated sentence from the 443 | data kept on (which is only necessary if not sent in the 444 | channel itself). If word1 and word2 are specified, they will be used 445 | to start the Markov chain. 446 | """ 447 | f = self._markov(channel, irc, word1, word2, 448 | prefixNick=False, Random=False) 449 | self.q.enqueue(f) 450 | markov = wrap(markov, ['channeldb', optional('something'), 451 | additional('something')]) 452 | 453 | def firsts(self, irc, msg, args, channel): 454 | """[] 455 | 456 | Returns the number of Markov's first links in the database for 457 | . 458 | """ 459 | def firsts(db): 460 | irc.reply( 461 | format('There are %s firsts in my Markov database for %s.', 462 | db.firsts(channel), channel)) 463 | self.q.enqueue(firsts) 464 | firsts = wrap(firsts, ['channeldb']) 465 | 466 | def lasts(self, irc, msg, args, channel): 467 | """[] 468 | 469 | Returns the number of Markov's last links in the database for 470 | . 471 | """ 472 | def lasts(db): 473 | irc.reply( 474 | format('There are %i lasts in my Markov database for %s.', 475 | db.lasts(channel), channel)) 476 | self.q.enqueue(lasts) 477 | lasts = wrap(lasts, ['channeldb']) 478 | 479 | def pairs(self, irc, msg, args, channel): 480 | """[] 481 | 482 | Returns the number of Markov's chain links in the database for 483 | . 484 | """ 485 | def pairs(db): 486 | irc.reply( 487 | format('There are %i pairs in my Markov database for %s.', 488 | db.pairs(channel), channel)) 489 | self.q.enqueue(pairs) 490 | pairs = wrap(pairs, ['channeldb']) 491 | 492 | def follows(self, irc, msg, args, channel): 493 | """[] 494 | 495 | Returns the number of Markov's third links in the database for 496 | . 497 | """ 498 | def follows(db): 499 | irc.reply( 500 | format('There are %i follows in my Markov database for %s.', 501 | db.follows(channel), channel)) 502 | self.q.enqueue(follows) 503 | follows = wrap(follows, ['channeldb']) 504 | 505 | def stats(self, irc, msg, args, channel): 506 | """[] 507 | 508 | Returns all stats (firsts, lasts, pairs, follows) for 's 509 | Markov database. 510 | """ 511 | def stats(db): 512 | irc.reply( 513 | format('Firsts: %i; Lasts: %i; Pairs: %i; Follows: %i', 514 | db.firsts(channel), db.lasts(channel), 515 | db.pairs(channel), db.follows(channel))) 516 | self.q.enqueue(stats) 517 | stats = wrap(stats, ['channeldb']) 518 | 519 | 520 | Class = Markov 521 | 522 | 523 | # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 524 | -------------------------------------------------------------------------------- /Limnoria/Markov/test.py: -------------------------------------------------------------------------------- 1 | ### 2 | # Copyright (c) 2005, James Vega 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # 8 | # * Redistributions of source code must retain the above copyright notice, 9 | # this list of conditions, and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions, and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the author of this software nor the name of 14 | # contributors to this software may be used to endorse or promote products 15 | # derived from this software without specific prior written consent. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | ### 29 | 30 | from supybot.test import * 31 | 32 | class MarkovTestCase(ChannelPluginTestCase): 33 | plugins = ('Markov',) 34 | config = {'supybot.plugins.Markov.ignoreBotCommands': True} 35 | def testMarkov(self): 36 | self.assertSnarfNoResponse('Feed the db some text') 37 | self.assertNotError('markov') 38 | self.assertNotError('markov Feed the') 39 | self.assertNotError('markov Feed') 40 | self.assertError('markov foo bar') 41 | self.assertRegexp('markov stats', r'Firsts: \d+;') 42 | 43 | 44 | # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 45 | -------------------------------------------------------------------------------- /Textual/oban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | if len(sys.argv) < 3 or not sys.argv[2]: 6 | print('/echo nick required to ban.') 7 | sys.exit() 8 | 9 | chan = sys.argv[1] 10 | nick = sys.argv[2] 11 | 12 | print('/echo banning {}...'.format(nick)) 13 | print('/cs op {}'.format(chan)) 14 | print('/timer 3 ban {}'.format(nick)) 15 | print('/timer 5 cs deop {}'.format(chan)) 16 | -------------------------------------------------------------------------------- /Textual/readme.md: -------------------------------------------------------------------------------- 1 | Textual 2 | ======= 3 | 4 | Place scripts in *~/Library/Application\ Scripts/com.codeux.irc.textual/* to install. 5 | -------------------------------------------------------------------------------- /Textual/ver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | if len(sys.argv) < 3 or not sys.argv[2]: 6 | print('/echo nick required.') 7 | sys.exit() 8 | 9 | nick = sys.argv[2] 10 | 11 | print('/ctcp {} VERSION'.format(nick)) 12 | -------------------------------------------------------------------------------- /XChat/README.md: -------------------------------------------------------------------------------- 1 | I recommend you upgrade to [HexChat](http://hexchat.github.io) and use my updated [HexChat scripts](https://github.com/TingPing/plugins/tree/master/HexChat) instead. 2 | -------------------------------------------------------------------------------- /XChat/audacious.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | import xchat 3 | 4 | __module_name__ = "Audacious" 5 | __module_author__ = "TingPing" 6 | __module_version__ = "0" 7 | __module_description__ = "Announce Audacious Songs" 8 | 9 | # May merge with Pithos plugin in future 10 | session_bus = dbus.SessionBus() 11 | 12 | def audacious(word, word_eol, userdata): 13 | try: 14 | player = session_bus.get_object('org.mpris.audacious', '/Player') 15 | song = player.GetMetadata() 16 | except (dbus.exceptions.DBusException, TypeError): 17 | xchat.prnt('Audacious: Could not find player.') 18 | return xchat.EAT_XCHAT 19 | 20 | try: 21 | msg = 'me is now playing %s by %s on %s.'%(song['title'], song['artist'], song['album']) 22 | except KeyError: 23 | xchat.prnt('Audacious: Could not find song info.') 24 | return None 25 | 26 | if len(word) > 1: 27 | if word[1] == 'info': 28 | xchat.prnt(msg[18:]) 29 | else: 30 | xchat.prnt('Audacious: Valid commands are: info, or without args to announce') 31 | else: 32 | xchat.command(msg) 33 | return xchat.EAT_ALL 34 | 35 | xchat.hook_command("audacious", audacious) 36 | xchat.hook_command("aud", audacious) 37 | # xchat.hook_command("np", audacious) # enable if you prefer but may conflict 38 | xchat.prnt(__module_name__ + ' version ' + __module_version__ + ' loaded.') -------------------------------------------------------------------------------- /XChat/define.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import ast 3 | import xchat 4 | 5 | __module_name__ = "Define" 6 | __module_author__ = "TingPing" 7 | __module_version__ = "2" 8 | __module_description__ = "Show word definitions" 9 | # based on google dictionary script by Sridarshan Shetty - http://sridarshan.co.cc 10 | 11 | def define(word, word_eol, userdata): 12 | 13 | if len(word) >= 2: 14 | _word = xchat.strip(word[1]) 15 | _number = 1 16 | if len(word) >= 3: 17 | _number = int(xchat.strip(word[2])) 18 | else: 19 | xchat.prnt('Define Usage: /define word [number]') 20 | xchat.prnt(' number being alternate definition') 21 | return xchat.EAT_ALL 22 | 23 | url="http://www.google.com/dictionary/json?callback=s&q=" + _word + "&sl=en&tl=en&restrict=pr,de&client=te" 24 | obj=urllib.urlopen(url); 25 | content=obj.read() 26 | obj.close() 27 | content=content[2:-10] 28 | dic=ast.literal_eval(content) 29 | if dic.has_key("webDefinitions"): 30 | webdef=dic["webDefinitions"] 31 | webdef=webdef[0] 32 | webdef=webdef["entries"] 33 | index=1 34 | 35 | for i in webdef: 36 | if index == _number: 37 | if i["type"]=="meaning": 38 | ans=i["terms"] 39 | op=ans[0]['text'] 40 | split=op.split(';') 41 | xchat.prnt(_word + ': ' + split[0].strip()) 42 | index+=1 43 | return xchat.EAT_ALL 44 | else: 45 | xchat.prnt('Description unavailable for ' + _word) 46 | return xchat.EAT_ALL 47 | 48 | xchat.hook_command("define", define) 49 | xchat.prnt(__module_name__ + ' version ' + __module_version__ + ' loaded.') 50 | -------------------------------------------------------------------------------- /XChat/growl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; -*- 2 | 3 | __module_name__='Growl' 4 | __module_description__='Growl notification support' 5 | __module_author__='TingPing' 6 | __module_version__='13' 7 | 8 | import re 9 | from time import time 10 | 11 | import xchat 12 | try: 13 | import gntp.notifier 14 | except: 15 | xchat.prnt('Growl Error: Please install https://github.com/kfdm/gntp') 16 | 17 | xchatlogo = 'http://forum.xchat.org/styles/prosilver/imageset/site_logo.png' 18 | lasttime = time() 19 | lastnick = '' 20 | 21 | # initial setup of growl and list of possible notifications 22 | # hostname and password are for over the network notifications 23 | growl = gntp.notifier.GrowlNotifier( 24 | applicationName='XChat', 25 | notifications=['Highlight', 'Private Message', 'Invited', 'Topic Changed', 26 | 'User Online', 'Server Notice', 'Disconnected', 'Banned', 27 | 'Killed', 'Kicked'], 28 | defaultNotifications=['Highlight', 'Private Message', 'Invited', 'Server Notice', 29 | 'Disconnected', 'Killed', 'Kicked', 'Banned'], 30 | applicationIcon=xchatlogo, 31 | #hostname='localhost', 32 | #password='' 33 | ) 34 | 35 | try: 36 | growl.register() 37 | except: 38 | xchat.prnt('Growl Error: Could not register with Growl') 39 | 40 | 41 | def growlnotify(_type, title, desc='', pri=0): 42 | try: 43 | growl.notify( 44 | noteType=_type, 45 | title=title, 46 | description=desc, 47 | icon=xchatlogo, 48 | sticky=False, 49 | priority=pri 50 | ) 51 | except: 52 | xchat.prnt('Growl Error: Growl is not running.') 53 | return None 54 | 55 | 56 | # now checks for and ignores mass hilights, performance impact not yet tested, maybe removed, optional, or only used on small channels 57 | # disabled for now 58 | # def masshilight(nick, message): 59 | # userlist = '' 60 | 61 | # for user in xchat.get_list('users'): 62 | # if user.nick != word[0]: 63 | # userlist += user.nick + ' ' 64 | 65 | # if re.search(userlist[:-1], xchat.strip(message)): 66 | # return True 67 | 68 | # else: 69 | # return False 70 | 71 | def spam(currenttime, currentnick): 72 | # Highlight and PM now have spam protection which previously could hang XChat 73 | global lasttime 74 | global lastnick 75 | 76 | if xchat.nickcmp(lastnick, currentnick) != 0: 77 | lasttime = time() 78 | lastnick = currentnick 79 | return False 80 | 81 | elif lasttime + 3 < currenttime: 82 | lasttime = time() 83 | return False 84 | 85 | else: 86 | lasttime = time() 87 | return True 88 | 89 | def active(chan): 90 | # Checks to see if chat is active to reduce annoying notifications 91 | try: 92 | chat = xchat.find_context() 93 | currentchat = chat.get_info("channel") 94 | status = xchat.get_info("win_status") 95 | if currentchat == chan and status == "active": 96 | return True 97 | else: 98 | return False 99 | except: 100 | return False 101 | 102 | 103 | # start list of notifications 104 | def hilight_callback(word, word_eol, userdata): 105 | if not spam(time(), word[0]): # and not masshilight(word[0], word[1]): 106 | growlnotify('Highlight', 107 | 'Highlight by ' + word[0], 108 | word[1], 109 | 1) 110 | 111 | def pm_callback(word, word_eol, userdata): 112 | if not spam(time(), word[0]) and not active(word[0]): 113 | growlnotify('Private Message', 114 | 'Messaged by ' + word[0], 115 | word[1], 116 | 1) 117 | 118 | def invited_callback(word, word_eol, userdata): 119 | growlnotify('Invited', 120 | 'Invited to ' + word[0], 121 | 'Invited to %s by %s on %s' % (word[0], word[1], word[2])) 122 | 123 | def topic_callback(word, word_eol, userdata): 124 | growlnotify('Topic Changed', 125 | word[2] + '\'s topic changed', 126 | '%s \'s topic changed to %s by %s' % (word[2], word[1], word[0]), 127 | -2) 128 | 129 | def onlinenotify_callback(word, word_eol, userdata): 130 | growlnotify('User Online', 131 | word[0] + ' is online on ' + word[2]) 132 | 133 | def servernotice_callback(word, word_eol, userdata): 134 | growlnotify('Server Notice', 135 | 'Notice from ' + word[1], 136 | word[0]) 137 | 138 | def disconnect_callback(word, word_eol, userdata): 139 | growlnotify('Disconnected', 140 | 'Disonnected from server', 141 | word[0], 142 | 1) 143 | 144 | def killed_callback(word, word_eol, userdata): 145 | growlnotify('Killed', 146 | 'Killed by ' + word[0], 147 | word[1], 148 | 2) 149 | 150 | def kicked_callback(word, word_eol, userdata): 151 | growlnotify('Kicked', 152 | 'You have been kicked from ' + word[2], 153 | 'Kicked by %s for %s' % (word[1], word[3]), 154 | 1) 155 | 156 | def banned_callback(word, word_eol, userdata): 157 | # this now works on a basic level, will possibly be improved 158 | nick = xchat.get_info('nick') 159 | for user in xchat.get_list('users'): 160 | if xchat.nickcmp(nick, user.nick) == 0: 161 | userhost = user.host 162 | hostip = re.split('@', userhost)[1] 163 | 164 | if re.search(nick, word[1]) or re.search(hostip, word[1]): 165 | growlnotify('Banned', 166 | 'You have been banned by ' + word[0]) 167 | 168 | # get events from xchat to call notifications 169 | xchat.hook_print("Channel Msg Hilight", hilight_callback) 170 | xchat.hook_print("Channel Action Hilight", hilight_callback) 171 | xchat.hook_print("Private Message to Dialog", pm_callback) 172 | xchat.hook_print("Private Action to Dialog", pm_callback) 173 | xchat.hook_print("Invited", invited_callback) 174 | xchat.hook_print("Notice", servernotice_callback) 175 | xchat.hook_print("Notify Online", onlinenotify_callback) 176 | xchat.hook_print("Topic Change", topic_callback) 177 | xchat.hook_print("You Kicked", kicked_callback) 178 | xchat.hook_print("Killed", killed_callback) 179 | xchat.hook_print("Channel Ban", banned_callback) 180 | # Nothing broke yet, its loaded! =) 181 | xchat.prnt(__module_name__ + ' version ' + __module_version__ + ' loaded.') -------------------------------------------------------------------------------- /XChat/pithos.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | import xchat 3 | 4 | __module_name__ = "Pithos" 5 | __module_author__ = "TingPing" 6 | __module_version__ = "2" 7 | __module_description__ = "Announce Pithos Songs" 8 | 9 | session_bus = dbus.SessionBus() 10 | 11 | def pithos(word, word_eol, userdata): 12 | try: 13 | player = session_bus.get_object('net.kevinmehall.Pithos', '/net/kevinmehall/Pithos') 14 | except (dbus.exceptions.DBusException, TypeError): 15 | xchat.prnt('Pithos: Could not find player.') 16 | return xchat.EAT_XCHAT 17 | 18 | song = player.GetCurrentSong() 19 | # to be configurable 20 | msg = 'me is now playing %s by %s on %s.'%(song['title'], song['artist'], song['album']) 21 | 22 | if len(word) > 1: 23 | # not very useful? 24 | if word[1] == 'info': 25 | xchat.prnt(msg[18:]) 26 | 27 | elif word[1] == 'next': 28 | player.SkipSong() 29 | 30 | elif word[1] == 'love': 31 | player.LoveCurrentSong() 32 | 33 | elif word[1] == 'hate': 34 | player.BanCurrentSong() 35 | 36 | else: 37 | xchat.prnt('Pithos: Valid commands are: info, next, love, hate, or without args to announce') 38 | else: 39 | xchat.command(msg) 40 | return xchat.EAT_ALL 41 | 42 | xchat.hook_command("pithos", pithos) 43 | # xchat.hook_command("np", pithos) # enable if you prefer but may conflict 44 | xchat.prnt(__module_name__ + ' version ' + __module_version__ + ' loaded.') -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | --------------------------------------------------------------------------------