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.
|
See the `/help plugins` command for further plugin management options.
|
||||||
|
|
||||||
|
## Plugins description
|
||||||
|
|
||||||
|
To be described.
|
||||||
|
|
||||||
## Building CProof with plugin support
|
## Building CProof with plugin support
|
||||||
|
|
||||||
Dependencies required:
|
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/)
|
- [Python hooks](https://jabber.space/api-reference/python/plugin/)
|
||||||
- [C API](https://jabber.space/api-reference/c/profapi/)
|
- [C API](https://jabber.space/api-reference/c/profapi/)
|
||||||
- [C hooks](https://jabber.space/api-reference/c/profhooks/)
|
- [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.
|
Print something in ASCII text.
|
||||||
|
|
||||||
Running from the console will print the text to the console.
|
Running from the console will print the text to the console.
|
||||||
Running in a chat or room window will send the ASCII text
|
Running in a chat or room window will send the ASCII text
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
autocorrector - Replace common typos
|
autocorrector - Replace common typos
|
||||||
|
|
||||||
My typing style is such a mess. But I invest more time in correcting it
|
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
|
programmatically than training myself to type better. Maybe thats the
|
||||||
problem? Anyway, here is a plugin for all keyboard messies like me.
|
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" ]
|
[ "<url>", "URL to open in the browser" ]
|
||||||
]
|
]
|
||||||
examples = [
|
examples = [
|
||||||
"/browser http://www.profanity.im"
|
"/browser http://jabber.space/"
|
||||||
]
|
]
|
||||||
|
|
||||||
prof.register_command("/browser", 0, 1, synopsis, description, args, examples, _cmd_browser)
|
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
|
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 -*-
|
# -*- coding:utf-8 -*-
|
||||||
# vim: set ts=2 sw=2 sts=2 et:
|
# 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.
|
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
|
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
|
to format text, specially when using OTR. This plugin removes them and also
|
||||||
unescapes HTML like <, >, etc that these clients also tend to escape.
|
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
|
sudo apt-get install scrot
|
||||||
|
|
||||||
Uses built in screencapture on OSX
|
Uses built in screencapture on OSX
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import prof
|
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:
|
Default settings require mpg123 to be installed on the host system:
|
||||||
sudo apt-get install mpg123
|
sudo apt-get install mpg123
|
||||||
brew install mpg123
|
brew install mpg123
|
||||||
@@ -209,7 +210,7 @@ def prof_init(version, status, account_name, fulljid):
|
|||||||
"/sounds clear private",
|
"/sounds clear private",
|
||||||
"/sounds rooms add <roomjid>",
|
"/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 = [
|
args = [
|
||||||
["on|off", "Enable or disable playing sounds."],
|
["on|off", "Enable or disable playing sounds."],
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user