Include external plugins, minor cleanup
- Consolidate plugins from other repositories to make their installation easier - Make minor changes to improve formatting
This commit is contained in:
13
README.md
13
README.md
@@ -19,6 +19,10 @@ Alternatively, you can use URLs to raw files to install Python plugins. For exam
|
||||
|
||||
See the `/help plugins` command for further plugin management options.
|
||||
|
||||
## Plugins description
|
||||
|
||||
To be described.
|
||||
|
||||
## Building CProof with plugin support
|
||||
|
||||
Dependencies required:
|
||||
@@ -41,3 +45,12 @@ API Documentation is available on our website, [jabber.space](https://jabber.spa
|
||||
- [Python hooks](https://jabber.space/api-reference/python/plugin/)
|
||||
- [C API](https://jabber.space/api-reference/c/profapi/)
|
||||
- [C hooks](https://jabber.space/api-reference/c/profhooks/)
|
||||
|
||||
## External plugins (C)
|
||||
|
||||
- [profanity-plugins by Reimar](https://github.com/ReimarPB/profanity-plugins) - Collection of plugins
|
||||
|
||||
|
||||
## External plugins (Python)
|
||||
|
||||
- [profanity-copy-message-plugin](https://github.com/lonski/profanity-copy-message-plugin) - Copy messages to clipboard.
|
||||
|
||||
549
src/antispam.py
Normal file
549
src/antispam.py
Normal file
@@ -0,0 +1,549 @@
|
||||
"""
|
||||
Antispam plugin for CProof.
|
||||
|
||||
Asks a question from new contacts to verify their humanity.
|
||||
"""
|
||||
|
||||
import math
|
||||
import prof
|
||||
import re
|
||||
import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
# Constants
|
||||
_antispam_defaults = {
|
||||
'msg': 'Please, solve the captcha',
|
||||
'question': 'Where a bullet must be lodged in order for rifle to shoot?',
|
||||
'answer': 'Chamber',
|
||||
'donemsg': 'You passed antispam. Congratulations!',
|
||||
'blockmsg': 'You\'ve been blocked. Try again {} minutes later',
|
||||
'errormsg': 'Wrong answer. Please, try again. The question is',
|
||||
'otrmsg': 'Please, turn off the OTR and then send the answer.',
|
||||
'unbanmsg': 'Your ban has been lifted. You can try again.',
|
||||
'maxtries': 5,
|
||||
'blocktime': 5,
|
||||
'debug': 'off',
|
||||
'dry': 'off'
|
||||
}
|
||||
|
||||
_time_format = "%d.%m.%Y %H:%M:%S"
|
||||
|
||||
_roster = []
|
||||
|
||||
_page_limit = 10
|
||||
|
||||
_egg_counter = 0
|
||||
|
||||
_as_settings_strings = ['msg', 'question', 'answer', 'donemsg', 'blockmsg', 'otrmsg', 'errormsg', 'unbanmsg', 'debug', 'dry']
|
||||
_as_settings_ints = {'maxtries': {'min': 2, 'max': 1000}, 'blocktime': {'min': -1, 'max': 100000}}
|
||||
_as_settings = [*_as_settings_strings, *_as_settings_ints]
|
||||
|
||||
|
||||
# Handlers
|
||||
|
||||
def prof_init(version, status, account_name, fulljid):
|
||||
synopsis = [
|
||||
"/antispam on|off",
|
||||
"/antispam settings",
|
||||
*[f"/antispam set {x} <msg>" for x in _as_settings_strings],
|
||||
"/antispam set maxtries <tries>",
|
||||
"/antispam set blocktime <minutes>",
|
||||
"/antispam counter [<page|clear>]",
|
||||
"/antispam history [<page|clear>]",
|
||||
"/antispam blocklist [<page>|add|remove <jid or part of jid>|clear]",
|
||||
"/antispam whitelist [<page>|add|remove <jid or part of jid>|clear]",
|
||||
]
|
||||
description = "Prevents spam messages by asking new contacts questions."
|
||||
args = [
|
||||
[ "set msg <msg>", "Set captcha message" ],
|
||||
[ "set question <msg>", "Set custom captcha question" ],
|
||||
[ "set answer <msg>", "Set answer to captcha question" ],
|
||||
[ "set donemsg <msg>", "Set congratulation message for passing antispam" ],
|
||||
[ "set blockmsg <msg>", "Set message that user is blocked. Use \"{}\" to show amounts of minutes before unlock. Example: You are banned. Unban in {} minutes." ],
|
||||
[ "set otrmsg <msg>", "Set answer in case if unauthorized user tries to initiate OTR session." ],
|
||||
[ "set errormsg <msg>", "Set prefix to answer in case if wrong answer given." ],
|
||||
[ "set unbanmsg <msg>", "Set message given when temporary ban is finished. Set to \"None\" to prevent sending message." ],
|
||||
[ "set maxtries <tries>", "Set maximum amount of tries before ban. Set -1 to make unlimited." ],
|
||||
[ "set blocktime <minutes>", "Set time to block user for. Set -1 to make ban unlimited." ],
|
||||
[ "set debug <on|off>", "Set debug mode on|off." ],
|
||||
[ "set dry <on|off>", "Set dry mode on|off. In dry mode antispam doesn't block messages, mode is needed for debug use." ],
|
||||
[ "counter [<page>|clear]", "Show or clear wrong answer counter" ],
|
||||
[ "history [<page>|clear]", "Show or clear history" ],
|
||||
[ "blocklist [add|remove <jid or part of jid>|clear]", "Show banned users, or add|remove user from ban" ],
|
||||
[ "whitelist [add|remove <jid or part of jid>|clear]", "Whitelist users. Whitelist have priority over bans." ],
|
||||
]
|
||||
examples = [
|
||||
"/antispam on",
|
||||
"/antispam set msg Please, solve the captcha.",
|
||||
"/antispam set question 2 + 2? (1984)",
|
||||
"/antispam set answer 5",
|
||||
"/antispam set donemsg You passed antispam. Now your messages are delivered.",
|
||||
"/antispam set blockmsg You've been blocked by antispam. Please, try again later in {} minute(s).",
|
||||
"/antispam set maxtries 5",
|
||||
"/antispam set blocktime 69",
|
||||
"/antispam blocklist clear",
|
||||
"/antispam history 42",
|
||||
]
|
||||
|
||||
prof.register_command("/antispam", 1, 40, synopsis, description, args, examples, _cmd_antispam)
|
||||
|
||||
prof.register_command("/as", 1, 40, _prep_alias(synopsis), description, args, _prep_alias(examples), _cmd_antispam)
|
||||
|
||||
prof.completer_add("/antispam",
|
||||
[ "set", "history", "blocklist", "whitelist" ]
|
||||
)
|
||||
|
||||
prof.completer_add("/antispam set",
|
||||
_as_settings
|
||||
)
|
||||
prof.completer_add("/antispam history",
|
||||
[ 'clear' ]
|
||||
)
|
||||
|
||||
prof.completer_add("/antispam blocklist",
|
||||
[ 'add', 'remove', 'clear' ]
|
||||
)
|
||||
|
||||
prof.completer_add("/antispam whitelist",
|
||||
[ 'add', 'remove', 'clear' ]
|
||||
)
|
||||
|
||||
prof.register_timed(_update_banned_list, 60)
|
||||
|
||||
_force_update_roster((fulljid.split('/')[0] if fulljid else ''))
|
||||
|
||||
prof.cons_show("[AntiSpam] plugin started.")
|
||||
if _is_dry_mode():
|
||||
prof.cons_show("[AntiSpam] WARNING: dry-mode is on, no messages are going to be blocked.")
|
||||
|
||||
def prof_on_message_stanza_send(stanza):
|
||||
body = _parse_tag_content(stanza, 'body')
|
||||
if body and (to := _parse_attr(stanza, 'to').split('/', 1)[0]):
|
||||
if not _is_authorized(to):
|
||||
prof.settings_string_list_add("antispam", "authorized", to)
|
||||
return stanza
|
||||
|
||||
def prof_on_presence_stanza_send(stanza):
|
||||
if _parse_attr(stanza, 'type') == 'subscribe' and (to := _parse_attr(stanza, 'to').split('/', 1)[0]):
|
||||
if not _is_authorized(to):
|
||||
prof.settings_string_list_add("antispam", "authorized", to)
|
||||
return stanza
|
||||
|
||||
|
||||
def prof_on_message_stanza_receive(stanza) -> bool:
|
||||
return _antispam_check_wrapper(stanza, 'msg')
|
||||
|
||||
def prof_on_presence_stanza_receive(stanza) -> bool:
|
||||
return _antispam_check_wrapper(stanza, 'presence')
|
||||
|
||||
def prof_on_iq_stanza_receive(stanza) -> bool:
|
||||
if 'jabber:iq:roster' in stanza and 'type="result"' in stanza:
|
||||
_update_roster(stanza)
|
||||
return _antispam_check_wrapper(stanza, 'iq')
|
||||
|
||||
# Custom functions
|
||||
|
||||
def _cmd_antispam(*args):
|
||||
if not len(args):
|
||||
_show_cmd_error()
|
||||
if args[0] == "on":
|
||||
prof.settings_boolean_set("antispam", "enabled", True)
|
||||
prof.cons_show("Antispam is enabled.")
|
||||
return
|
||||
if args[0] == "off":
|
||||
prof.settings_boolean_set("antispam", "enabled", False)
|
||||
prof.cons_show("Antispam is disabled.")
|
||||
return
|
||||
if args[0] == "set":
|
||||
if len(args) < 3:
|
||||
_show_cmd_error('More arguments required')
|
||||
return
|
||||
key = args[1]
|
||||
if key not in _as_settings:
|
||||
_show_cmd_error(f'Wrong setting. You can use only {_as_settings}')
|
||||
return
|
||||
if args[2] == "default":
|
||||
_set_default_values(key)
|
||||
return
|
||||
if _set_values(key, args[2:]):
|
||||
prof.cons_show(f'Antispam setting {key} was successfully changed to "{" ".join(args[2:])}"')
|
||||
else:
|
||||
_show_cmd_error("Something went wrong, setting wasn't changed")
|
||||
return
|
||||
if args[0] == "settings":
|
||||
prof.cons_show("===== Antispam settings =====")
|
||||
for key in _as_settings:
|
||||
value = _get_string_setting(key) if key in _as_settings_strings else _get_int_setting(key)
|
||||
fvalue = f'"{value}"' if key in _as_settings_strings else value
|
||||
prof.cons_show(f'{key} is {fvalue}')
|
||||
return
|
||||
if args[0] in ["counter", "history", "authorized"]:
|
||||
return _cmd_show_or_clear(args)
|
||||
if args[0] == "blocklist":
|
||||
return _cmd_as_blocklist(args)
|
||||
if args[0] == "whitelist":
|
||||
return _cmd_as_whitelist(args)
|
||||
if args[0] == "egg":
|
||||
global _egg_counter
|
||||
if _egg_counter == 10:
|
||||
prof.cons_show(f"Do you know that it's an antispam plugin and not an egg farm? No more eggs for you!")
|
||||
return
|
||||
_egg_counter += 1
|
||||
prof.cons_show(f"Egg number {_egg_counter} has been planted.")
|
||||
if _egg_counter == 5:
|
||||
prof.cons_show(f"What do you need so many eggs for?")
|
||||
return
|
||||
if args[0] == "roster":
|
||||
prof.cons_show("\n".join(_roster))
|
||||
return
|
||||
prof.cons_bad_cmd_usage('/antispam')
|
||||
|
||||
def _cmd_show_or_clear(args):
|
||||
if len(args) == 1:
|
||||
return _show_page(args[0])
|
||||
if len(args) == 2:
|
||||
if args[1] == "clear":
|
||||
if prof.settings_string_list_clear("antispam", args[0]):
|
||||
prof.cons_show(f'{args[0].capitalize()} has been cleared.')
|
||||
else:
|
||||
prof.cons_show(f'{args[0].capitalize()} is already empty.')
|
||||
return
|
||||
return _show_page(args[0], args[1])
|
||||
prof.cons_bad_cmd_usage('/antispam')
|
||||
|
||||
def _cmd_as_blocklist(args):
|
||||
if len(args) < 3:
|
||||
return _cmd_show_or_clear(args)
|
||||
if args[1] == "add":
|
||||
for jid in args[2:]:
|
||||
ban_time = _ban_user(jid, full_jid=False, time_limited=False)
|
||||
prof.cons_show(f'JID "{jid}" has been banned {ban_time}.')
|
||||
elif args[1] == "remove":
|
||||
for jid in args[2:]:
|
||||
if _unban_user(jid):
|
||||
prof.cons_show(f'JID "{jid}" has been unbanned.')
|
||||
else:
|
||||
prof.cons_show(f'JID "{jid}" has not been found in the blocklist.')
|
||||
|
||||
def _cmd_as_whitelist(args):
|
||||
if len(args) < 3:
|
||||
return _cmd_show_or_clear(args)
|
||||
if args[1] == "add":
|
||||
for jid in args[2:]:
|
||||
prof.settings_string_list_add("antispam", "whitelist", jid)
|
||||
prof.cons_show(f'JID "{jid}" has been whitelisted.')
|
||||
elif args[1] == "remove":
|
||||
for jid in args[2:]:
|
||||
if prof.settings_string_list_remove("antispam", "whitelist", jid):
|
||||
prof.cons_show(f'JID "{jid}" has been removed from the whitelist.')
|
||||
else:
|
||||
prof.cons_show(f'JID "{jid}" has not been found in the whitelist.')
|
||||
|
||||
def _show_cmd_error(error="", user_mistake=True) -> None:
|
||||
"""Used to shows str:error when user typed wrong input"""
|
||||
if user_mistake:
|
||||
prof.cons_bad_cmd_usage('/antispam')
|
||||
if error:
|
||||
prof.cons_show(error)
|
||||
|
||||
|
||||
def _set_default_values(key) -> bool:
|
||||
default_value = _antispam_defaults[key]
|
||||
if (key in _as_settings_strings):
|
||||
prof.settings_string_set("antispam", key, default_value)
|
||||
elif (key in _as_settings_ints):
|
||||
prof.settings_int_set("antispam", key, default_value)
|
||||
prof.cons_show(f'Antispam setting {key} was successfully set to its default ({default_value}).')
|
||||
|
||||
def _set_values(key, value) -> bool:
|
||||
"""Returns Bool True if values has been set, False is value is invalid"""
|
||||
value = ' '.join(value)
|
||||
if key in _as_settings_strings:
|
||||
prof.settings_string_set("antispam", key, value)
|
||||
return True
|
||||
if key in _as_settings_ints:
|
||||
min_val, max_val = _as_settings_ints[key]['min'], _as_settings_ints[key]['max']
|
||||
if not _represents_int_in_range(value, min_val, max_val):
|
||||
prof.cons_show(f'Value of {key} must be an integer between {min_val} and {max_val}!')
|
||||
return False
|
||||
prof.settings_int_set("antispam", key, int(value))
|
||||
return True
|
||||
return False
|
||||
|
||||
def _parse_tag_content(stanza, tag):
|
||||
tmp = re.search(f"<{tag}.*?>([\S\s]*?)</{tag}>", stanza) # don't blame me, it works :D
|
||||
try:
|
||||
return tmp.group(1) if tmp else ""
|
||||
except IndexError as e:
|
||||
return ""
|
||||
|
||||
def _parse_attr(stanza, attribute) -> str:
|
||||
"""Parse attribute from the first tag"""
|
||||
tmp = re.search(f'^\s*<[^>]+{attribute}="([^"]+)"', stanza)
|
||||
try:
|
||||
return tmp.group(1) if tmp else ""
|
||||
except IndexError as e:
|
||||
return ""
|
||||
|
||||
def _parse_sender(stanza) -> str:
|
||||
"""Returns sender from stanza or empty string if not found"""
|
||||
if "<forwarded" in stanza:
|
||||
stanza = _parse_tag_content(stanza, "forwarded")
|
||||
return _parse_attr(stanza, "from").split('/', 1)[0]
|
||||
|
||||
def _parse_id(stanza) -> str:
|
||||
"""Returns id from stanza or empty string if not found"""
|
||||
return _parse_attr(stanza, "id")
|
||||
|
||||
def _is_dry_mode() -> bool:
|
||||
return _get_string_setting("dry") == "on"
|
||||
|
||||
def _antispam_check_wrapper(stanza, stanza_type) -> bool:
|
||||
result = _antispam_check(stanza, stanza_type)
|
||||
dry_mode = _is_dry_mode()
|
||||
if not result:
|
||||
if _get_string_setting("debug") == "on" or dry_mode:
|
||||
prof.cons_show(f"[AntiSpam{' (DryMode)' if dry_mode else ''}] Blocked stanza: {stanza}")
|
||||
else:
|
||||
prof.log_debug(f"[AntiSpam] Blocked stanza: {stanza}")
|
||||
return result or _is_dry_mode()
|
||||
|
||||
def _antispam_check(stanza, stanza_type) -> bool:
|
||||
"""Returns Bool True if message is not spam, False if message should be blocked (spam/unverified sender)"""
|
||||
sender = _parse_sender(stanza)
|
||||
stanza_id = _parse_id(stanza)
|
||||
if not sender and "from=" in stanza:
|
||||
prof.log_warning(f"In the following stanza From is present, but not catched: \n{stanza}")
|
||||
if not _is_activated():
|
||||
return True
|
||||
if _is_whitelisted(sender) or _is_in_roster(sender):
|
||||
return True
|
||||
if _is_banned(sender):
|
||||
return False
|
||||
if _is_authorized(sender):
|
||||
return True
|
||||
if not _roster:
|
||||
prof.log_debug(f"[AntiSpam] Passed message because roster was empty. Stanza: {stanza}")
|
||||
return True
|
||||
if stanza_type == "presence":
|
||||
if re.search('^\s*<presence.+type="subscribe".+>', stanza):
|
||||
_send_stanza(f'<presence type="unsubscribed" to="{sender}" />')
|
||||
_send_stanza(f'<message type="chat" to="{sender}"><subject>{_get_string_setting("msg")}</subject><body>{_get_string_setting("question")}</body></message>')
|
||||
_counter_inc(sender)
|
||||
elif stanza_type == "iq":
|
||||
if re.search('^\s*<iq[^>]+type="set"', stanza):
|
||||
to = f'to="{sender}" ' if sender else ''
|
||||
_send_stanza(f'<iq type="error" id="{stanza_id}" {to}/>')
|
||||
return False
|
||||
return True
|
||||
elif stanza_type == "msg":
|
||||
if _parse_attr(stanza, "type") == "error":
|
||||
prof.settings_string_list_add("antispam", "history", f"{_get_time()}|{sender}|{_parse_tag_content(stanza, 'body')}")
|
||||
return False
|
||||
if not _is_correct_answer(stanza):
|
||||
if (is_otr_requested := "OTRv2" in stanza):
|
||||
_send_stanza(f'<message type="chat" to="{sender}"><body>{_get_string_setting("otrmsg")}</body></message>')
|
||||
prof.settings_string_list_add("antispam", "history", f"{_get_time()}|{sender}|{_parse_tag_content(stanza, 'body')}")
|
||||
wrong_tries = _get_counter(sender)
|
||||
subject = f'<subject>{_get_string_setting("msg")}</subject>' if wrong_tries == 0 else ''
|
||||
wrong_answer = _get_string_setting("errormsg")+'\n' if wrong_tries > 0 and not is_otr_requested else ''
|
||||
_send_stanza(f'<message type="chat" to="{sender}">{subject}<body>{wrong_answer}{_get_string_setting("question")}</body></message>')
|
||||
_counter_inc(sender)
|
||||
return False
|
||||
prof.settings_string_list_add("antispam", "authorized", sender)
|
||||
_send_stanza(f'<message type="chat" to="{sender}"><body>{_get_string_setting("donemsg")}</body></message>')
|
||||
return False
|
||||
|
||||
def _force_update_roster(myjid=None) -> None:
|
||||
global _roster
|
||||
if not _roster and myjid:
|
||||
_roster = [myjid]
|
||||
prof.send_stanza(f'<iq id="{uuid4()}" type="get"><query xmlns="jabber:iq:roster"/>\</iq>')
|
||||
return
|
||||
|
||||
def _update_roster(stanza) -> None:
|
||||
global _roster
|
||||
_roster = [
|
||||
*re.findall('<iq[^>]+to="([^"/]+)["/]', stanza),
|
||||
*[x for x,y in re.findall('<item[^>]+jid="([^"]+?)"[^>]+subscription="([^"]+?)"', stanza) if y == "both" or y == "to"],
|
||||
*[x for y,x in re.findall('<item[^>]+subscription="([^"]+?)"[^>]+jid="([^"]+?)"', stanza) if y == "both" or y == "to"],
|
||||
]
|
||||
|
||||
|
||||
def _is_activated() -> bool:
|
||||
return prof.settings_boolean_get("antispam", "enabled", True)
|
||||
|
||||
def _update_banned_list() -> None:
|
||||
ban_list = prof.settings_string_list_get("antispam", "blocklist") or []
|
||||
for banned_user in ban_list:
|
||||
if not (tmp := _unpack_banned_user(banned_user)):
|
||||
continue
|
||||
ban_time, ban_type, jid = tmp
|
||||
if ban_time != "Forever" and not _is_future_time(ban_time):
|
||||
unbanmsg = _get_string_setting("unbanmsg")
|
||||
if unbanmsg != "None":
|
||||
_send_stanza(f'<message type="chat" to="{jid}"><body>{unbanmsg}</body></message>')
|
||||
prof.settings_string_list_remove("antispam", "blocklist", banned_user)
|
||||
return
|
||||
|
||||
def _unpack_banned_user(banned_user):
|
||||
"""Returns None or Tuple of banned user info in (time_banned, ban_type, jid) format"""
|
||||
tmp = banned_user.split("|", 2)
|
||||
if len(tmp) != 3:
|
||||
prof.cons_show(f"Problem on banned user unpacking: \"{banned_user}\". Deleting...")
|
||||
prof.settings_string_list_remove("antispam", "blocklist", banned_user)
|
||||
return
|
||||
return tmp
|
||||
|
||||
def _is_in_roster(jid) -> bool:
|
||||
return jid in _roster
|
||||
|
||||
def _is_authorized(jid) -> bool:
|
||||
return jid in (prof.settings_string_list_get("antispam", "authorized") or [])
|
||||
|
||||
def _is_banned(jid) -> bool:
|
||||
ban_list = prof.settings_string_list_get("antispam", "blocklist") or []
|
||||
for banned_user in ban_list:
|
||||
if not (tmp := _unpack_banned_user(banned_user)):
|
||||
continue
|
||||
ban_time, ban_type, bjid = tmp
|
||||
if ban_type == "F": # On full JID ban (e.g. "user@example.com")
|
||||
if jid == bjid and (ban_time == "Forever" or _is_future_time(ban_time)):
|
||||
return True
|
||||
else: # On partial JID ban (e.g. "@example.com", "spammer")
|
||||
if bjid in jid and (ban_time == "Forever" or _is_future_time(ban_time)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_whitelisted(jid) -> bool:
|
||||
wl_users = (prof.settings_string_list_get("antispam", "whitelist") or [])
|
||||
return any(wj in jid for wj in wl_users)
|
||||
|
||||
def _get_validate_count(c):
|
||||
tmp = c.split('|', 1)
|
||||
if len(tmp) != 2:
|
||||
prof.log_warning(f'Invalid counter entry: "{c}". Removing...')
|
||||
prof.settings_string_list_remove("antispam", "counter", c)
|
||||
return
|
||||
return int(tmp[0]), tmp[1]
|
||||
|
||||
def _counter_inc(jid) -> None:
|
||||
counter = prof.settings_string_list_get("antispam", "counter") or []
|
||||
for c in counter:
|
||||
if not (tmp := _get_validate_count(c)):
|
||||
continue
|
||||
count, cjid = tmp
|
||||
if jid == cjid:
|
||||
prof.settings_string_list_remove("antispam", "counter", c)
|
||||
prof.settings_string_list_add("antispam", "counter", f"{count+1}|{jid}")
|
||||
if count % _get_int_setting("maxtries") == 0:
|
||||
ban_time = _ban_user(jid)
|
||||
blockmsg = _get_string_setting("blockmsg").replace('{}', str(ban_time))
|
||||
_send_stanza(f'<message type="chat" to="{jid}"><body>{blockmsg}</body></message>')
|
||||
return
|
||||
prof.settings_string_list_add("antispam", "counter", f"1|{jid}")
|
||||
|
||||
|
||||
def _get_counter(jid) -> int:
|
||||
counter = prof.settings_string_list_get("antispam", "counter") or []
|
||||
for c in counter:
|
||||
if not (tmp := _get_validate_count(c)):
|
||||
continue
|
||||
count, cjid = tmp
|
||||
if jid == cjid:
|
||||
return count
|
||||
return 0
|
||||
|
||||
def _show_page(table, page_num=1) -> None:
|
||||
"""Shows a page from table N (table is fetched from string_list setting with name) to plugin user through cons_show."""
|
||||
if not _represents_int_in_range(page_num, 1):
|
||||
prof.cons_show("Use correct page number. It must be integer from 1 to 10000.")
|
||||
return
|
||||
page_num = int(page_num)
|
||||
page = page_num-1
|
||||
table_content = prof.settings_string_list_get("antispam", table) or []
|
||||
table_size = len(table_content)
|
||||
total_pages = math.ceil(table_size/_page_limit)
|
||||
if not table_content:
|
||||
return prof.cons_show(f"Nothing to display, {table} is empty")
|
||||
if page_num > total_pages:
|
||||
return prof.cons_show(f"Page {page_num} does not exist. There are only {total_pages} pages")
|
||||
page_content = table_content[page*_page_limit:page_num*_page_limit]
|
||||
if not page_content:
|
||||
return prof.cons_show(f"Nothing to display, {table} on Page {page_num} is empty")
|
||||
for x in page_content:
|
||||
formatting = (lambda x: x)
|
||||
prof.cons_show(formatting(x))
|
||||
prof.cons_show(f"Page {page_num}/{total_pages}. Currently {table_size} entries.")
|
||||
|
||||
def _ban_user(jid, full_jid=True, time_limited=True, custom_time_limit=None):
|
||||
"""Adds user to ban list"""
|
||||
ban_type = "F" if full_jid else "P"
|
||||
if time_limited and (ban_time := custom_time_limit if custom_time_limit else _get_int_setting("blocktime")) != -1:
|
||||
ban_time_formatted = (datetime.datetime.now() + datetime.timedelta(minutes=ban_time)).strftime("%d.%m.%Y %H:%M:%S")
|
||||
_unban_user(jid)
|
||||
prof.settings_string_list_add("antispam", "blocklist", f"{ban_time_formatted}|{ban_type}|{jid}")
|
||||
return ban_time
|
||||
prof.settings_string_list_add("antispam", "blocklist", f"Forever|{ban_type}|{jid}")
|
||||
return "forever"
|
||||
|
||||
def _unban_user(jid):
|
||||
"""Removes user from ban list. True on success, otherwise false."""
|
||||
ban_list = prof.settings_string_list_get("antispam", "blocklist") or []
|
||||
for banned_user in ban_list:
|
||||
if not (tmp := _unpack_banned_user(banned_user)):
|
||||
continue
|
||||
ban_time, ban_type, bjid = tmp
|
||||
if bjid == jid:
|
||||
return prof.settings_string_list_remove("antispam", "blocklist", banned_user)
|
||||
return False
|
||||
|
||||
|
||||
# Settings utils
|
||||
|
||||
def _get_string_setting(setting) -> str:
|
||||
return prof.settings_string_get("antispam", setting, _antispam_defaults[setting])
|
||||
|
||||
def _get_int_setting(setting) -> int:
|
||||
return prof.settings_int_get("antispam", setting, _antispam_defaults[setting])
|
||||
|
||||
# Utils
|
||||
|
||||
def _prep_alias(replace_list):
|
||||
return [x.replace("/antispam", "/as") for x in replace_list]
|
||||
|
||||
def _is_correct_answer(stanza):
|
||||
correct_answer = _get_string_setting('answer')
|
||||
answer = _parse_tag_content(stanza, "body")
|
||||
if not answer:
|
||||
prof.log_warning(f'Invalid mesage stanza. Ignoring user input. Stanza: {stanza}')
|
||||
return False
|
||||
return answer.strip().lower() == correct_answer.strip().lower()
|
||||
|
||||
def _is_future_time(time) -> bool:
|
||||
"""Checks formatted time and returns True if time from parameter is in the future"""
|
||||
try:
|
||||
ptime = datetime.datetime.strptime(time, _time_format)
|
||||
except ValueError as e:
|
||||
prof.log_error(f"Exception raised when time was compared (input time: {time})\n{e}")
|
||||
return True
|
||||
return ptime > datetime.datetime.now()
|
||||
|
||||
def _get_time():
|
||||
"""Returns current time formatted"""
|
||||
return datetime.datetime.now().strftime(_time_format)
|
||||
|
||||
def _represents_int_in_range(s, num_min=-1, num_max=10000) -> bool:
|
||||
"""Returns True if passed number is integer and it is in provided range (from -1 to 10000 by default)"""
|
||||
try:
|
||||
num = int(s)
|
||||
return num >= num_min and num <= num_max
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _send_stanza(stanza):
|
||||
if not _is_dry_mode():
|
||||
return prof.send_stanza(stanza)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Print something in ASCII text.
|
||||
|
||||
Running from the console will print the text to the console.
|
||||
Running in a chat or room window will send the ASCII text
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
autocorrector - Replace common typos
|
||||
|
||||
My typing style is such a mess. But I invest more time in correcting it
|
||||
programmatically than training myself to type better. Maybe thats the
|
||||
problem? Anyway, here is a plugin for all keyboard messies like me.
|
||||
|
||||
@@ -72,7 +72,7 @@ def prof_init(version, status, account_name, fulljid):
|
||||
[ "<url>", "URL to open in the browser" ]
|
||||
]
|
||||
examples = [
|
||||
"/browser http://www.profanity.im"
|
||||
"/browser http://jabber.space/"
|
||||
]
|
||||
|
||||
prof.register_command("/browser", 0, 1, synopsis, description, args, examples, _cmd_browser)
|
||||
|
||||
7
src/c/moj/.gitignore
vendored
Normal file
7
src/c/moj/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
moj
|
||||
moj.exe
|
||||
*.o
|
||||
*.so
|
||||
gen/lookup.c
|
||||
gen/emoji.json
|
||||
gen/emoji.csv
|
||||
15
src/c/moj/COPYING
Normal file
15
src/c/moj/COPYING
Normal file
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2018, Adrian "vifino" Pistol
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
68
src/c/moj/Makefile
Normal file
68
src/c/moj/Makefile
Normal file
@@ -0,0 +1,68 @@
|
||||
# moj makefile
|
||||
CC ?= cc
|
||||
|
||||
MOJILIST=https://raw.githubusercontent.com/milesj/emojibase/master/packages/data/en/raw.json
|
||||
|
||||
# Do not touch.
|
||||
all: moj libmoj.so
|
||||
|
||||
# Generate lookup lists
|
||||
gen/emoji.json:
|
||||
mkdir -p gen
|
||||
wget -O $@ ${MOJILIST}
|
||||
|
||||
gen/emoji.csv: gen/emoji.json
|
||||
jq -r '.[] | select(.emoji) | [.emoji] + .shortcodes | @csv' -- \
|
||||
gen/emoji.json | \
|
||||
sed 's/^"//;s/"$$//' | awk -f src/process.awk > $@
|
||||
|
||||
gen/emoji.gperf: src/gen_gperf.sh gen/emoji.csv
|
||||
./src/gen_gperf.sh > ./gen/emoji.gperf
|
||||
|
||||
gen/emoji_list.h: src/gen_list.sh gen/emoji.csv
|
||||
./src/gen_list.sh > $@
|
||||
|
||||
gen/lookup.c: gen/emoji.gperf
|
||||
gperf --global-table --readonly-tables \
|
||||
--struct-type --includes \
|
||||
--compare-lengths --compare-strncmp \
|
||||
--ignore-case -Q emoji_spool \
|
||||
-H emoji_name_hash -N emoji_lookup_ref -m 3 \
|
||||
--output-file=$@ $^
|
||||
|
||||
# Compile our code.
|
||||
src/moj.o: gen/lookup.c gen/emoji_list.h src/moj.c
|
||||
$(CC) -c ${CFLAGS} ${CPPFLAGS} -o $@ src/moj.c
|
||||
|
||||
libmoj.so: src/moj.o
|
||||
$(CC) -shared -fPIC ${CFLAGS} ${CPPFLAGS} -o $@ ${LDFLAGS} $<
|
||||
|
||||
moj: src/main.c src/moj.o
|
||||
$(CC) ${CFLAGS} ${CPPFLAGS} -o $@ ${LDFLAGS} $^
|
||||
|
||||
# Profanity plugin.
|
||||
profanity_emoji.so: src/profanity_emoji.c src/moj.o
|
||||
$(CC) -shared -fPIC ${CFLAGS} ${CPPFLAGS} -o $@ ${LDFLAGS} $^
|
||||
|
||||
profanity: profanity_emoji.so
|
||||
|
||||
# Install
|
||||
install: moj
|
||||
install -d $(DESTDIR)/$(PREFIX)/bin
|
||||
install moj $(DESTDIR)/$(PREFIX)/bin
|
||||
|
||||
install-devel: libmoj.so
|
||||
install -d $(DESTDIR)/$(PREFIX)/lib
|
||||
install libmoj.so $(DESTDIR)/$(PREFIX)/lib
|
||||
install -d $(DESTDIR)/$(PREFIX)/include
|
||||
cp src/moj.h $(DESTDIR)/$(PREFIX)/include/libmoj.h
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)/$(PREFIX)/bin/moj
|
||||
uninstall-devel:
|
||||
rm -f $(DESTDIR)/$(PREFIX)/lib/libmoj.so $(DESTDIR)/$(PREFIX)/include/libmoj.h
|
||||
|
||||
|
||||
# Cleanup
|
||||
clean:
|
||||
rm -f moj gen/lookup.c src/*.o profanity_emoji.so
|
||||
28
src/c/moj/README.md
Normal file
28
src/c/moj/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# moj
|
||||
'moj is for those of us that want something more minimal than emojify.
|
||||
|
||||
Reasons to use moj instead of emojify:
|
||||
- only 3 letters(!), huge reduction compared to 7 of emojify.
|
||||
- written in C, compile time hash table generation, no interpreted language.
|
||||
- VERY FAST. How does 0.002s to generate the average commit messages of this project sound? This is on a ThinkPad T61. A potato.
|
||||
- data not mixed with code, only generated data/glue and handwritten code. (seperate files)
|
||||
|
||||
# Requirements
|
||||
- Make (compile time)
|
||||
- C99 compiler(?). (compile time)
|
||||
- gperf (compile time)
|
||||
|
||||
No runtime deps.
|
||||
|
||||
To regen the emoji list you also need:
|
||||
- wget
|
||||
- jq
|
||||
- awk
|
||||
|
||||
# Credits
|
||||
[milesj/emojibase](https://github.com/milesj/emojibase) for the JSON Emoji list. Many thanks
|
||||
|
||||
vifino for doing everything else.
|
||||
|
||||
# License
|
||||
ISC.
|
||||
1810
src/c/moj/gen/emoji.gperf
Normal file
1810
src/c/moj/gen/emoji.gperf
Normal file
File diff suppressed because it is too large
Load Diff
1646
src/c/moj/gen/emoji_list.h
Normal file
1646
src/c/moj/gen/emoji_list.h
Normal file
File diff suppressed because it is too large
Load Diff
15
src/c/moj/src/gen_gperf.sh
Executable file
15
src/c/moj/src/gen_gperf.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# Generator for the hash list.
|
||||
# This is unquestionably magic.
|
||||
set -e
|
||||
cat <<HEADER
|
||||
%{
|
||||
// THIS FILE IS GENERATED BY src/gen_list.sh.
|
||||
// DO NOT TOUCH HERE.
|
||||
%}
|
||||
struct emoji_ref { char* name; unsigned int ref; };
|
||||
%%
|
||||
HEADER
|
||||
|
||||
cat gen/emoji.csv | sed 's/"//g' | \
|
||||
awk -F',' '{ count++; for (i = 2; i <= NF; i++) printf "%s, %i\n", $i, count;}'
|
||||
26
src/c/moj/src/gen_list.sh
Executable file
26
src/c/moj/src/gen_list.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
# Generator for the actual emoji list.
|
||||
set -e
|
||||
|
||||
EMOJIS=$(wc -l < gen/emoji.csv)
|
||||
|
||||
cat <<HEADER
|
||||
// THIS FILE IS GENERATED BY src/gen_list.sh.
|
||||
// DO NOT TOUCH HERE.
|
||||
|
||||
#define NUM_EMOJI $EMOJIS
|
||||
static const char* emoji_list[$EMOJIS] = {
|
||||
HEADER
|
||||
|
||||
gen() {
|
||||
printf "\t\"%s\",\n" $1
|
||||
}
|
||||
|
||||
cat gen/emoji.csv | sed 's/,/ /g' | while read -r input; do
|
||||
gen $(eval "echo $input")
|
||||
done
|
||||
|
||||
cat <<FOOTER
|
||||
};
|
||||
FOOTER
|
||||
|
||||
124
src/c/moj/src/main.c
Normal file
124
src/c/moj/src/main.c
Normal file
@@ -0,0 +1,124 @@
|
||||
// moj.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <getopt.h>
|
||||
#include "moj.h"
|
||||
|
||||
#define MODE_NORMAL 0
|
||||
#define MODE_LOOKUP 1
|
||||
#define MODE_USELIBRARY 2
|
||||
|
||||
void usage(char* name, int fail) {
|
||||
FILE* f = stdout;
|
||||
if (fail) f = stderr;
|
||||
fprintf(f, "Usage: %s [-s sep] [-e] <:emoji: text>\n"
|
||||
" -e: lookup mode - interpret arguments as emoji names\n"
|
||||
" -s <sep>: use custom seperator\n", name);
|
||||
exit(fail);
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
usage(argv[0], 1);
|
||||
}
|
||||
|
||||
// parse opts
|
||||
int opt;
|
||||
int mode = MODE_NORMAL;
|
||||
char* sep = NULL;
|
||||
while ((opt = getopt(argc, argv, "hels:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'h':
|
||||
usage(argv[0], 0);
|
||||
break;
|
||||
default:
|
||||
case '?':
|
||||
usage(argv[0], 1);
|
||||
break;
|
||||
case 'e':
|
||||
mode = MODE_LOOKUP;
|
||||
break;
|
||||
case 's':
|
||||
sep = optarg;
|
||||
break;
|
||||
case 'l':
|
||||
mode = MODE_USELIBRARY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (optind >= argc)
|
||||
usage(argv[0], 1);
|
||||
|
||||
int argi;
|
||||
switch (mode) {
|
||||
case MODE_LOOKUP:
|
||||
for (argi = optind; argi < argc; argi++) {
|
||||
char* found = moj_lookup(argv[argi], strlen(argv[argi]));
|
||||
if (found) {
|
||||
printf("%s%s", found, sep ? sep : "\n");
|
||||
} else {
|
||||
printf("Couldn't find %s. :(\n", argv[argi]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (sep) printf("\n");
|
||||
break;
|
||||
case MODE_NORMAL:
|
||||
for (argi = optind; argi < argc; argi++) {
|
||||
char* s = argv[argi];
|
||||
char c;
|
||||
int ci;
|
||||
int len = strlen(s);
|
||||
int start = 0;
|
||||
int attention = 0;
|
||||
for (ci = 0; ci <= len; ci++) {
|
||||
c = s[ci];
|
||||
|
||||
if (attention) { // in :
|
||||
if (c == ' ') {
|
||||
attention = 0;
|
||||
s[ci] = 0;
|
||||
fprintf(stdout, "%s ", s + start);
|
||||
} else if (c == ':') {
|
||||
// check if start to ci - start -1 is a valid emoji.
|
||||
s[ci] = 0;
|
||||
char* found = moj_lookup(s + start + 1, ci - start - 1);
|
||||
if (found) {
|
||||
fprintf(stdout, "%s", found);
|
||||
} else {
|
||||
// not an emoji? oops.
|
||||
fprintf(stdout, "%s:", s + start);
|
||||
start = ci;
|
||||
// the next might be one, though..
|
||||
}
|
||||
attention = 0;
|
||||
}
|
||||
} else if (c == ':' && ci < len) {
|
||||
attention = 1;
|
||||
start = ci;
|
||||
} else {
|
||||
putc(c, stdout);
|
||||
}
|
||||
}
|
||||
if (attention && start < len)
|
||||
fprintf(stdout, "%s", s + start + 1);
|
||||
|
||||
if (argi < (argc - 1))
|
||||
fputs(sep ? sep : " ", stdout);
|
||||
}
|
||||
fprintf(stdout, "\n");
|
||||
break;
|
||||
case MODE_USELIBRARY:
|
||||
for (argi = optind; argi < argc; argi++) {
|
||||
char* rep = moj_replace(argv[argi], strlen(argv[argi]));
|
||||
printf("%s\n", rep);
|
||||
free(rep);
|
||||
}
|
||||
break;;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
98
src/c/moj/src/moj.c
Normal file
98
src/c/moj/src/moj.c
Normal file
@@ -0,0 +1,98 @@
|
||||
// moj: Small Emoji library
|
||||
// Based on our generated things.
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include "../gen/lookup.c"
|
||||
#include "../gen/emoji_list.h"
|
||||
|
||||
const char* moj_lookup(const char* name, size_t len) {
|
||||
const struct emoji_ref *found = emoji_lookup_ref(name, len);
|
||||
if (found) {
|
||||
return emoji_list[found->ref - 1];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* moj_replace(const char* text, size_t len) {
|
||||
if (!len)
|
||||
return strdup(text);
|
||||
|
||||
char c;
|
||||
// count size delta to alloc a string just the right size.
|
||||
int size_delta = 0;
|
||||
|
||||
size_t ci;
|
||||
size_t start = 0;
|
||||
size_t strend;
|
||||
int attention = 0;
|
||||
for (ci = 0; ci <= len; ci++) {
|
||||
c = text[ci];
|
||||
|
||||
if (attention) { // in :
|
||||
if (c == ' ') {
|
||||
attention = 0;
|
||||
} else if (c == ':') {
|
||||
// check if start to ci - start -1 is a valid emoji.
|
||||
size_t elen = ci - start - 1;
|
||||
const char* found = moj_lookup(text + start + 1, elen);
|
||||
if (found) {
|
||||
size_delta += elen - strlen(found);
|
||||
} else {
|
||||
start = ci;
|
||||
}
|
||||
attention = 0;
|
||||
}
|
||||
} else if (c == ':' && ci < len) {
|
||||
attention = 1;
|
||||
start = ci;
|
||||
}
|
||||
}
|
||||
|
||||
// allocate buffer the right size.
|
||||
size_t realsz = len + size_delta;
|
||||
char* buf = malloc(realsz + 1);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
|
||||
start = 0;
|
||||
attention = 0;
|
||||
size_t offset = 0;
|
||||
for (ci = 0; ci < len; ci++) {
|
||||
c = text[ci];
|
||||
|
||||
if (attention) { // in :
|
||||
if (c == ' ') {
|
||||
attention = 0;
|
||||
memcpy(buf + offset, text + start, ci - start + 1);
|
||||
offset += ci - start + 1;
|
||||
} else if (c == ':') {
|
||||
// check if start to ci - start -1 is a valid emoji.
|
||||
const char* found = moj_lookup(text + start + 1, ci - start - 1);
|
||||
if (found) {
|
||||
offset += sprintf(buf + offset, "%s", found);
|
||||
} else {
|
||||
// not an emoji? oops.
|
||||
memcpy(buf + offset, text + start, ci - start + 1);
|
||||
offset += ci - start + 1;
|
||||
start = ci;
|
||||
// the next might be one, though..
|
||||
}
|
||||
attention = 0;
|
||||
}
|
||||
} else if (c == ':' && ci < len) {
|
||||
attention = 1;
|
||||
start = ci;
|
||||
} else {
|
||||
buf[offset] = c;
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
if (attention) {
|
||||
memcpy(buf + offset, text + start, ci - start);
|
||||
offset += ci - start;
|
||||
}
|
||||
buf[realsz] = 0;
|
||||
return buf;
|
||||
}
|
||||
5
src/c/moj/src/moj.h
Normal file
5
src/c/moj/src/moj.h
Normal file
@@ -0,0 +1,5 @@
|
||||
// moj: a small emoji library and cli.
|
||||
#include <stddef.h>
|
||||
|
||||
char* moj_lookup(const char* name, size_t len);
|
||||
char* moj_replace(const char* name, size_t len);
|
||||
93
src/c/moj/src/process.awk
Normal file
93
src/c/moj/src/process.awk
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/awk -f
|
||||
# This file takes in our CSV, emits simpler stuff.
|
||||
# It also fixes up some things.
|
||||
|
||||
# Bunch of convenience functions.
|
||||
function didsee(name) {
|
||||
if (name in seen)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function hasnew() {
|
||||
for (i = 2; i <= NF; i++)
|
||||
if (!didsee($i))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function newname(name) {
|
||||
if (didsee(name))
|
||||
return 0;
|
||||
|
||||
seen[name] = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function char(str, idx) {
|
||||
return substr(str, idx, 1);
|
||||
}
|
||||
|
||||
function prefixed(name, prefixes) {
|
||||
for (prefix in prefixes)
|
||||
if (index(name, prefixes[prefix]) == 1)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function inarray(name, array) {
|
||||
for (i in array)
|
||||
if (array[i] == name)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function noing(name) {
|
||||
# yep thats the name.
|
||||
len = length(name);
|
||||
if (len > 5 && substr(name, len - 2, 3) == "ing" && !prefixed(name, persons) && !inarray(name, exemptions)) {
|
||||
if (char(name, len - 4) == char(name, len - 3)) {
|
||||
# double letter combo, gg, nn, etc..
|
||||
short = substr(name, 0, len - 4);
|
||||
} else {
|
||||
short = substr(name, 0, len - 3);
|
||||
}
|
||||
if (newname(short))
|
||||
printf ",%s", short;
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
FS="\",\""
|
||||
|
||||
# I hate this.
|
||||
persons[1] = "person_";
|
||||
persons[2] = "women_";
|
||||
persons[3] = "woman_"
|
||||
persons[4] = "men_";
|
||||
persons[5] = "man_";
|
||||
|
||||
# I hate this even more.
|
||||
exemptions[1] = "bowing";
|
||||
exemptions[2] = "golfing";
|
||||
}
|
||||
|
||||
{
|
||||
if (hasnew())
|
||||
printf "%s", $1;
|
||||
|
||||
for (i = 2; i <= NF; i++) {
|
||||
if (newname($i)) {
|
||||
printf ",%s", $i;
|
||||
noing($i);
|
||||
if (index($i, "person_") == 1) {
|
||||
replaced = substr($i, 8);
|
||||
if (newname(replaced)) {
|
||||
printf ",%s", replaced;
|
||||
}
|
||||
noing(replaced);
|
||||
}
|
||||
}
|
||||
}
|
||||
printf "\n";
|
||||
}
|
||||
10
src/c/moj/src/profanity_emoji.c
Normal file
10
src/c/moj/src/profanity_emoji.c
Normal file
@@ -0,0 +1,10 @@
|
||||
// moj: CProof plugin
|
||||
// Intercepts text and replaces :emoji:
|
||||
|
||||
#include <profapi.h>
|
||||
#include <string.h>
|
||||
#include "moj.h"
|
||||
|
||||
char* prof_pre_chat_message_send(const char* const barejid, const char* message) {
|
||||
return moj_replace(message, strlen(message)); // yep that is it.
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Simple plugin to display the PID of the Profanity process and its parent
|
||||
Simple plugin to display the PID of the CProof process and its parent
|
||||
|
||||
Theme example in ~/.local/share/profanity/plugin_themes
|
||||
|
||||
|
||||
167
src/history.py
Normal file
167
src/history.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
CProof plugin to read full chat history with custom editor using /hh command.
|
||||
"""
|
||||
|
||||
import os
|
||||
import prof
|
||||
import re
|
||||
import sqlite3
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
_editor = ""
|
||||
_current_user = ""
|
||||
_logs_dir = ""
|
||||
_db_file = ""
|
||||
_win = "History"
|
||||
_plugin_name = __file__.split('/')[-1] if __file__ else "history.py"
|
||||
|
||||
|
||||
def _handle_win_input():
|
||||
pass
|
||||
|
||||
|
||||
def _create_win(win):
|
||||
if not prof.win_exists(win):
|
||||
prof.win_create(win, _handle_win_input)
|
||||
|
||||
|
||||
def _show_error(error):
|
||||
error_msg = f"[History Reader] Error happened: {error}"
|
||||
prof.cons_alert()
|
||||
prof.cons_show(error_msg)
|
||||
prof.log_error(error_msg)
|
||||
return
|
||||
|
||||
|
||||
def _str_sanitize(text):
|
||||
return re.sub('[^a-zA-Z0-9\._]', '', text.replace("@", "_at_"))
|
||||
|
||||
|
||||
def _cmd_editor(*args):
|
||||
global _editor
|
||||
_db_connection = sqlite3.connect(f"{_db_file}")
|
||||
_db_connection.text_factory = bytes
|
||||
_cur = _db_connection.cursor()
|
||||
msg_buffer = []
|
||||
if args and args[0] == "set":
|
||||
if len(args) < 2:
|
||||
prof.cons_show("Please, use this format: \"/hh set <editor>\"")
|
||||
return
|
||||
_editor = args[1]
|
||||
prof.settings_string_set("history", "editor", _editor)
|
||||
prof.cons_show(
|
||||
f"New editor set up successfully. New editor: {_editor}")
|
||||
return
|
||||
if args and args[0] == "--no-repeat":
|
||||
prof.cons_show("[History Reader] Trying operation again...")
|
||||
if not _current_user:
|
||||
if args and args[0] == "--no-repeat":
|
||||
_show_error("Can't fetch current user.")
|
||||
return
|
||||
prof.send_line(f"/plugins reload {_plugin_name}")
|
||||
prof.send_line("/hh --no-repeat")
|
||||
return
|
||||
if not _editor:
|
||||
prof.cons_show(
|
||||
"Please, set up editor using /hh set. E.g. /hh set /usr/bin/vim")
|
||||
return
|
||||
if not (recipient := prof.get_current_recipient()):
|
||||
prof.cons_show("Please, use this command in a chat window")
|
||||
return
|
||||
|
||||
tmpfpath = _logs_dir / f"{_str_sanitize(recipient)}.log"
|
||||
res = _cur.execute("""SELECT timestamp, from_jid, message FROM `chatlogs`
|
||||
WHERE ((`from_jid` = :jid AND `to_jid` = :myjid)
|
||||
OR (`from_jid` = :myjid AND `to_jid` = :jid))
|
||||
ORDER BY id""", {"jid": recipient, "myjid": _current_user})
|
||||
|
||||
for bmsg in res.fetchall():
|
||||
try:
|
||||
msg = [x.decode("UTF-8", errors="backslashreplace") for x in bmsg]
|
||||
except Exception as e:
|
||||
msg = [x.decode("UTF-8", errors="replace") for x in bmsg]
|
||||
sender = "me" if msg[1] == _current_user else msg[1]
|
||||
msg_buffer.append(f"{msg[0]} - {sender}: {msg[2]}")
|
||||
|
||||
tmpfpath.write_text('\n'.join(msg_buffer),
|
||||
encoding="UTF-8", errors="replace")
|
||||
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
os.execlp(_editor, _editor, str(tmpfpath))
|
||||
else:
|
||||
if pid == -1:
|
||||
return
|
||||
os.waitpid(pid, 0)
|
||||
prof.send_line("/statusbar show name")
|
||||
try:
|
||||
tmpfpath.unlink()
|
||||
except Exception as e:
|
||||
prof.log_error(
|
||||
f"[History Reader] Error on file deletion (path: {tmpfpath}): {e}")
|
||||
return
|
||||
|
||||
|
||||
def prof_on_connect(account_name, fulljid):
|
||||
prof.log_debug(
|
||||
f"[History Reader] prof_on_connect called with this JID: {fulljid}")
|
||||
_init(fulljid)
|
||||
|
||||
|
||||
def prof_on_disconnect(account_name, fulljid):
|
||||
prof.log_debug(
|
||||
f"[History Reader] prof_on_disconnect called with this JID: {fulljid}")
|
||||
global _current_user
|
||||
_current_user = ""
|
||||
|
||||
|
||||
def prof_init(version, status, account_name, fulljid):
|
||||
prof.log_debug(
|
||||
f"[History Reader] prof_init called with this JID: {fulljid}")
|
||||
_init(fulljid)
|
||||
synopsis = ["/hh"]
|
||||
description = "Open an editor and check user's history."
|
||||
args = [
|
||||
["set", "Set custom editor."]
|
||||
]
|
||||
examples = [
|
||||
"/hh",
|
||||
"/hh set /usr/bin/vim",
|
||||
"/hh set gtk-open"
|
||||
]
|
||||
prof.register_command("/hh", 0, 2, synopsis,
|
||||
description, args, examples, _cmd_editor)
|
||||
prof.completer_add("/hh", ["set"])
|
||||
prof.filepath_completer_add("/hh set")
|
||||
|
||||
|
||||
def _init(fulljid):
|
||||
global _current_user, _editor, _logs_dir, _db_file
|
||||
prof.log_debug(
|
||||
f"[History Reader] Initialization started with this JID: {fulljid}")
|
||||
_current_user = fulljid and fulljid.split('/')[0]
|
||||
if not _current_user:
|
||||
prof.log_debug(
|
||||
"[History Reader] Can't fetch current user, aborting initialization...")
|
||||
return
|
||||
_cproof_home = Path(os.getenv('XDG_DATA_HOME')
|
||||
or "~/.local/share").expanduser() / "profanity"
|
||||
_logs_dir = _cproof_home / "chatlogs" / _str_sanitize(_current_user)
|
||||
db_dir = _cproof_home / "database" / _str_sanitize(_current_user)
|
||||
if not _logs_dir.is_dir():
|
||||
_show_error(f"Can't open logs directory. Path: {_logs_dir}")
|
||||
return
|
||||
if not db_dir.is_dir():
|
||||
_show_error(f"Can't open DB directory. Path: {db_dir}")
|
||||
return
|
||||
_db_file = db_dir / "chatlog.db"
|
||||
if not _db_file.is_file():
|
||||
_show_error(f"Log db is not present. Path: {_db_file}")
|
||||
return
|
||||
_editor = prof.settings_string_get("history", "editor", "")
|
||||
if not _editor:
|
||||
prof.cons_show("Please, set up editor using /hh set.")
|
||||
else:
|
||||
prof.cons_show(
|
||||
f"[History Reader] Successfully started. Editor: {_editor}")
|
||||
@@ -1,10 +1,10 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# vim: set ts=2 sw=2 sts=2 et:
|
||||
"""
|
||||
Author: Diego Blanco <diego.blanco@treitos.com>
|
||||
|
||||
Removes "unwanted" HTML tags from incoming messages and unescapes html.
|
||||
|
||||
Author: Diego Blanco <diego.blanco@treitos.com>
|
||||
|
||||
Some XMPP clients like Adium or Pidgin send HTML tags like <font>, <b>, <u>, etc
|
||||
to format text, specially when using OTR. This plugin removes them and also
|
||||
unescapes HTML like <, >, etc that these clients also tend to escape.
|
||||
|
||||
@@ -5,7 +5,6 @@ Requires scrot installed for screenshot (on linux)
|
||||
sudo apt-get install scrot
|
||||
|
||||
Uses built in screencapture on OSX
|
||||
|
||||
"""
|
||||
|
||||
import prof
|
||||
|
||||
137
src/notifycmd.py
Normal file
137
src/notifycmd.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Executes a command when a message is received
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Oliver Schmidhauser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import prof
|
||||
from subprocess import Popen
|
||||
import time
|
||||
from sys import platform
|
||||
|
||||
def secure(string):
|
||||
string=string.replace('\\','\\\\')
|
||||
string=string.replace("\"","\\\"")
|
||||
string=string.replace("$","\$")
|
||||
string=string.replace("`","\`")
|
||||
return string
|
||||
|
||||
def notifycmd(sender,message):
|
||||
command = prof.settings_string_get("notifycmd", "command", "")
|
||||
# replace markers with complex strings first to avoid "%%mittens"=>"(%%)mittens"=>"(%m)ittens"=>"<message>ittens" but rather do "(%%)mittens"=>"%mittens"
|
||||
command = command.replace("%%","{percentreplace}")
|
||||
command = command.replace("%s","${senderreplace}")
|
||||
command = command.replace("%m","${messagereplace}")
|
||||
|
||||
|
||||
command = command.replace("{percentreplace}","%")
|
||||
command = "set -f;senderreplace=\"{}\";messagereplace=\"{}\";{}".format(secure(sender),secure(message),command)
|
||||
p = Popen(['sh', '-c', command])
|
||||
|
||||
def prof_post_chat_message_display(barejid, resource, message):
|
||||
enabled = prof.settings_string_get("notifycmd", "enabled", "on")
|
||||
current_recipient = prof.get_current_recipient()
|
||||
if enabled == "on" or (enabled == "active" and current_recipient == barejid):
|
||||
notifycmd(barejid, message)
|
||||
return message
|
||||
|
||||
|
||||
def prof_post_room_message_display(barejid, nick, message):
|
||||
enabled = prof.settings_string_get("notifycmd", "enabled", "on")
|
||||
rooms = prof.settings_string_get("notifycmd", "rooms", "mention")
|
||||
current_muc = prof.get_current_muc()
|
||||
mynick = prof.get_room_nick(barejid)
|
||||
# Don't notify for ones own messages
|
||||
if nick != mynick:
|
||||
if rooms == "on":
|
||||
if enabled == "on":
|
||||
notifycmd(nick + " in " + barejid, message)
|
||||
elif enabled == "active" and current_muc == barejid:
|
||||
notifycmd(nick, message)
|
||||
elif rooms == "mention":
|
||||
if mynick in message:
|
||||
if enabled == "on":
|
||||
notifycmd(nick, message)
|
||||
elif enabled == "active" and current_muc == barejid:
|
||||
notifycmd(nick, message)
|
||||
return message
|
||||
|
||||
|
||||
def prof_post_priv_message_display(barejid, nick, message):
|
||||
if enabled:
|
||||
notifycmd(nick,message)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def _cmd_notifycmd(arg1=None, arg2=None):
|
||||
if arg1 == "on":
|
||||
prof.settings_string_set("notifycmd", "enabled", "on")
|
||||
prof.cons_show("Notifycmd plugin enabled")
|
||||
elif arg1 == "off":
|
||||
prof.settings_string_set("notifycmd", "enabled", "off")
|
||||
prof.cons_show("Notifycmd plugin disabled")
|
||||
elif arg1 == "active":
|
||||
prof.settings_string_set("notifycmd", "enabled", "active")
|
||||
prof.cons_show("Notifycmd plugin enabled for active window only")
|
||||
elif arg1 == "command":
|
||||
if arg2 == None:
|
||||
prof.cons_bad_cmd_usage("/notifycmd")
|
||||
else:
|
||||
prof.settings_string_set("notifycmd", "command", arg2)
|
||||
prof.cons_show("notifycmd plugin command set to: " + arg2)
|
||||
elif arg1 == "rooms":
|
||||
if arg2 == None:
|
||||
prof.cons_bad_cmd_usage("/notifycmd")
|
||||
else:
|
||||
prof.settings_string_set("notifycmd", "rooms", arg2)
|
||||
prof.cons_show("notifycmd plugin notifications for rooms set to: " + arg2)
|
||||
else:
|
||||
enabled = prof.settings_string_get("notifycmd", "enabled", "on")
|
||||
rooms = prof.settings_string_get("notifycmd", "rooms", "mention")
|
||||
command = prof.settings_string_get("notifycmd", "command", "")
|
||||
prof.cons_show("Notifycmd plugin settings:")
|
||||
prof.cons_show("enabled : " + enabled)
|
||||
prof.cons_show("command : " + command)
|
||||
prof.cons_show("rooms : " + rooms)
|
||||
|
||||
def prof_init(version, status, account_name, fulljid):
|
||||
synopsis = [
|
||||
"/notifycmd on|off|active",
|
||||
"/notifycmd command <command>",
|
||||
"/notifycmd rooms on|off|mention"
|
||||
]
|
||||
description = "Executes a command when a message is received"
|
||||
args = [
|
||||
[ "on|off", "Enable/disable notifycmd for all windows" ],
|
||||
[ "active", "Enable notifycmd for active window only" ],
|
||||
[ "command <args>", "Set command to execute. Replaces %s with sender, %m with message and %% with literal %." ],
|
||||
[ "rooms <args>", "Setting for multi-user rooms. Set to 'on', 'off' or 'mention'. If set to mention it will only run it your nick was mentioned." ]
|
||||
]
|
||||
examples = []
|
||||
|
||||
prof.register_command("/notifycmd", 0, 2, synopsis, description, args, examples, _cmd_notifycmd)
|
||||
prof.completer_add("/notifycmd", [ "on", "off","active","command","rooms" ])
|
||||
prof.completer_add("/notifycmd rooms", [ "on", "off", "mention" ])
|
||||
144
src/sanitize.py
Normal file
144
src/sanitize.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Sanitize incoming messages from Adium clients.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Nd2(SO4)3
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from io import StringIO
|
||||
from lxml import etree
|
||||
from lxml.etree import _ElementUnicodeResult, _ElementStringResult
|
||||
import prof
|
||||
|
||||
# ROADMAP
|
||||
#
|
||||
# Version 2:
|
||||
# "/sanitise on|off": global auto detect setting
|
||||
# "/sanitise <jid> on|off|<mode>": setting per JID: can be autodetect, off,
|
||||
# or a special "mode" (if there are any).
|
||||
|
||||
iq_count = 1
|
||||
|
||||
# Stores client information. JID/str~>Client/str.
|
||||
clients = {}
|
||||
|
||||
|
||||
def prof_on_presence_stanza_receive(stanza):
|
||||
"""Send a software version request when a contact comes online."""
|
||||
full_jid = etree.fromstring(stanza).get("from")
|
||||
if full_jid:
|
||||
send_version_request(full_jid)
|
||||
return True
|
||||
|
||||
|
||||
def send_version_request(full_jid):
|
||||
global iq_count
|
||||
|
||||
iq = etree.Element(
|
||||
"iq", {"type": "get", "to": full_jid, "id": "sanitiser_" + str(iq_count)}
|
||||
)
|
||||
etree.SubElement(iq, "query", {"xmlns": "jabber:iq:version"})
|
||||
|
||||
iq_count = iq_count + 1
|
||||
prof.send_stanza(etree.tostring(iq))
|
||||
|
||||
|
||||
def prof_on_iq_stanza_receive(stanza):
|
||||
"""Listen for software version responses and save results in the
|
||||
'clients' store.
|
||||
"""
|
||||
iq = etree.fromstring(stanza)
|
||||
id = iq.get("id")
|
||||
if not id:
|
||||
return True
|
||||
if not id.startswith("sanitiser_"):
|
||||
return True
|
||||
full_jid = iq.get("from")
|
||||
if not full_jid:
|
||||
return False
|
||||
|
||||
query = iq.find("{jabber:iq:version}query")
|
||||
if query is None:
|
||||
return False
|
||||
client = query.find("{jabber:iq:version}name")
|
||||
if client is not None:
|
||||
clients[full_jid] = client.text
|
||||
return False
|
||||
|
||||
|
||||
def prof_pre_chat_message_display(jid, resource, message):
|
||||
"""Sanitises chat room messages if necessary."""
|
||||
full_jid = "%s/%s" % (jid, resource)
|
||||
if full_jid not in clients:
|
||||
return message
|
||||
return get_sanitiser(clients[full_jid])(message)
|
||||
|
||||
|
||||
# Sanitiser functions:
|
||||
|
||||
|
||||
def get_sanitiser(client):
|
||||
"""Returns appropriate sanitiser function for the given
|
||||
Jabber client (string).
|
||||
"""
|
||||
if client.startswith("Adium"):
|
||||
return sanitise
|
||||
else:
|
||||
return lambda msg: msg
|
||||
|
||||
|
||||
def sanitise(message):
|
||||
"""Cleans message from HTML. Raises exceptions on problems."""
|
||||
parser = etree.HTMLParser()
|
||||
tree = etree.parse(StringIO(unicode(message)), parser)
|
||||
elems = tree.xpath("(//br|//a|//*[not(self::a)]/text())")
|
||||
return "".join(map(substitude, elems))
|
||||
|
||||
|
||||
def try_sanitise(message):
|
||||
"""Same as 'sanitise' but returns unmodified message if it encounters
|
||||
a problem, instead of throwing an exception.
|
||||
"""
|
||||
try:
|
||||
return sanitise(message)
|
||||
except:
|
||||
return message
|
||||
|
||||
|
||||
def substitude(elem):
|
||||
"""Substitudes an lxml element with a string representation."""
|
||||
if type(elem) in (_ElementUnicodeResult, _ElementStringResult):
|
||||
return str(elem)
|
||||
else:
|
||||
if elem.tag == "br":
|
||||
return "\n"
|
||||
elif elem.tag == "a":
|
||||
text = elem.text or ""
|
||||
href = elem.get("href", "")
|
||||
if not text and not href:
|
||||
return ""
|
||||
elif text == href:
|
||||
return href
|
||||
else:
|
||||
return "%s (%s)" % (text, href)
|
||||
else:
|
||||
raise Exception("unhandled tag <%s>" % elem.tag)
|
||||
128
src/shortcuts.py
Normal file
128
src/shortcuts.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Manage Message Shortcuts
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 René Calles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import prof
|
||||
|
||||
ENABLED = True
|
||||
SHORTCUTS_DICTIONARY = {u'shrug': u'¯\_(ツ)_/¯',
|
||||
u'yay': u'\o/'}
|
||||
|
||||
plugin_win = "Shortcuts"
|
||||
|
||||
|
||||
def _handle_win_input(win, line):
|
||||
prof.win_show(win, line)
|
||||
|
||||
|
||||
def create_win():
|
||||
if prof.win_exists(plugin_win) == False:
|
||||
prof.win_create(plugin_win, _handle_win_input)
|
||||
|
||||
|
||||
def _substitute(message):
|
||||
if ENABLED:
|
||||
shortcuts = re.findall(r'(?:^|\s)(:\w+:)(?=\s|$)', message)
|
||||
for s in shortcuts:
|
||||
shortcut_text = SHORTCUTS_DICTIONARY.get(s.strip()[1:-1])
|
||||
if shortcut_text:
|
||||
message = message.replace(s.strip(), shortcut_text)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def save(key, value):
|
||||
global SHORTCUTS_DICTIONARY
|
||||
SHORTCUTS_DICTIONARY[key] = value
|
||||
|
||||
|
||||
def _list_shortcuts():
|
||||
global SHORTCUTS_DICTIONARY
|
||||
# ordered_dict = sorted(DICTIONARY.items(), key=lambda t: t[0])
|
||||
prof.cons_show(u'Shortcuts: ')
|
||||
for key, value in SHORTCUTS_DICTIONARY.iteritems():
|
||||
prof.cons_show(u':{0}: => {1}'.format(key, value))
|
||||
|
||||
|
||||
def _cmd_shortcuts(arg1=None, arg2=None, arg3=None):
|
||||
global ENABLED
|
||||
|
||||
if arg1 == None:
|
||||
prof.cons_show("Shortcuts Plugin is {}.".format("ON" if ENABLED else "OFF"))
|
||||
elif arg1 == "on":
|
||||
ENABLED = True
|
||||
elif arg1 == "off":
|
||||
ENABLED = False
|
||||
elif arg1 == "list":
|
||||
_list_shortcuts()
|
||||
elif arg1 == "set":
|
||||
if arg2 == None:
|
||||
prof.cons_bad_cmd_usage("/shortcuts")
|
||||
else:
|
||||
save(arg2, arg3)
|
||||
else:
|
||||
prof.cons_bad_cmd_usage("/shortcuts")
|
||||
|
||||
|
||||
def prof_pre_chat_message_send(jid, message):
|
||||
return _substitute(message)
|
||||
|
||||
|
||||
def prof_pre_room_message_send(room, message):
|
||||
return _substitute(message)
|
||||
|
||||
|
||||
def prof_pre_priv_message_send(room, nick, message):
|
||||
return _substitute(message)
|
||||
|
||||
|
||||
def prof_init(version, status):
|
||||
synopsis = [
|
||||
"/shortcuts",
|
||||
"/shortcuts on|off",
|
||||
"/shortcuts set <name> <text>"
|
||||
]
|
||||
description = "Manage shortcuts for emoticons or other stuff"
|
||||
args = [
|
||||
[ "on|off", "Enable/disable shortcuts" ],
|
||||
[ "set <name> <text>", "Save shortcut" ],
|
||||
[ "list", "List Shortcuts Dictionary"]
|
||||
]
|
||||
examples = [
|
||||
"/shortcuts",
|
||||
"/shortcuts on",
|
||||
"/shortcuts set lol \"Laughing out loud\""
|
||||
]
|
||||
|
||||
prof.register_command("/shortcuts", 0, 3, synopsis, description, args, examples, _cmd_shortcuts)
|
||||
prof.register_ac("/shortcuts", [ "on", "off", "set" ])
|
||||
|
||||
shortcut_keys = [":{}:".format(k) for k in SHORTCUTS_DICTIONARY.keys()]
|
||||
# prof.cons_show(", ".join(shortcut_keys))
|
||||
prof.register_ac(" ", shortcut_keys)
|
||||
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Play sounds on various events.
|
||||
Play sounds on receiving messages.
|
||||
|
||||
Default settings require mpg123 to be installed on the host system:
|
||||
sudo apt-get install mpg123
|
||||
brew install mpg123
|
||||
@@ -209,7 +210,7 @@ def prof_init(version, status, account_name, fulljid):
|
||||
"/sounds clear private",
|
||||
"/sounds rooms add <roomjid>",
|
||||
]
|
||||
description = "Play sounds on various Profanity events. Calling with no args shows current sound files."
|
||||
description = "Play sounds on receiving messages. Executing the command without arguments displays current settings."
|
||||
args = [
|
||||
["on|off", "Enable or disable playing sounds."],
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user