diff options
Diffstat (limited to 'modules')
29 files changed, 343 insertions, 136 deletions
diff --git a/modules/alias_variables.py b/modules/alias_variables.py index 6f2499a3..b618a7c6 100644 --- a/modules/alias_variables.py +++ b/modules/alias_variables.py @@ -5,7 +5,11 @@ class Module(ModuleManager.BaseModule): @utils.hook("get.command") @utils.kwarg("priority", EventManager.PRIORITY_HIGH) def get_command(self, event): + event["kwargs"]["CTRIGGER"] = event["command_prefix"] + + event["kwargs"]["BNICK"] = event["server"].nickname event["kwargs"]["NICK"] = event["user"].nickname + if event["is_channel"]: event["kwargs"]["CHAN"] = event["target"].name random_user = random.choice(list(event["target"].users)) diff --git a/modules/channel_save.py b/modules/autojoin.py index d5b69638..bbd08dfb 100644 --- a/modules/channel_save.py +++ b/modules/autojoin.py @@ -25,6 +25,9 @@ class Module(ModuleManager.BaseModule): if channel_name in channels: channels.remove(channel_name) server.set_setting("autojoin", channels) + return True + else: + return False @utils.hook("self.part") def on_part(self, event): @@ -33,3 +36,14 @@ class Module(ModuleManager.BaseModule): @utils.hook("self.kick") def on_kick(self, event): self._remove_channel(event["server"], event["channel"].name) + + @utils.hook("raw.received.470") + def on_linkchannel(self, event): + initial = event["line"].args[1] + initial_lower = event["server"].irc_lower(initial) + linked = event["line"].args[2] + + if self._remove_channel(event["server"], initial_lower): + self.log.warn(f"{str(event['server'])} " + f"channel {initial} linked to {linked} " + "- removed from autojoin") diff --git a/modules/badwords.py b/modules/badwords.py index d23a4137..db5ed24c 100644 --- a/modules/badwords.py +++ b/modules/badwords.py @@ -80,9 +80,8 @@ class Module(ModuleManager.BaseModule): ban = True if ban: - event["channel"].send_ban("*!%s@%s" % ( - event["user"].username, - event["user"].realname)) + event["channel"].send_ban(self.exports.get("ban-mask")( + event["server"], event["channel"], event["user"])) if kick: event["channel"].send_kick(event["user"].nickname, "You said a badword!") diff --git a/modules/ban_enforce.py b/modules/ban_enforce.py new file mode 100644 index 00000000..4e40bb84 --- /dev/null +++ b/modules/ban_enforce.py @@ -0,0 +1,27 @@ +from src import ModuleManager, utils + +REASON = "User is banned from this channel" + +@utils.export("channelset", utils.BoolSetting("ban-enforce", + "Whether or not to parse new bans and kick who they affect")) +class Module(ModuleManager.BaseModule): + @utils.hook("received.mode.channel") + def on_mode(self, event): + if event["channel"].get_setting("ban-enforce", False): + bans = [] + kicks = set([]) + for mode, arg in event["modes"]: + if mode[0] == "+" and mode[1] == "b": + bans.append(arg) + + if bans: + umasks = {u.hostmask(): u for u in event["channel"].users} + for ban in bans: + mask = utils.irc.hostmask_parse(ban) + matches = list(utils.irc.hostmask_match_many( + umasks.keys(), mask)) + for match in matches: + kicks.add(umasks[match]) + if kicks: + nicks = [u.nickname for u in kicks] + event["channel"].send_kicks(sorted(nicks), REASON) diff --git a/modules/channel_op.py b/modules/channel_op.py index e7ab7bc9..d37e27b4 100644 --- a/modules/channel_op.py +++ b/modules/channel_op.py @@ -26,6 +26,28 @@ class TargetType(enum.Enum): MASK = 2 ACCOUNT = 3 +def _mlock(s): + modes, *args = s.split(" ") + + adds = "" + removes = "" + add = True + for c in modes: + if c == "+": + add = True + elif c == "-": + add = False + elif add: + adds += c + else: + removes += c + + return ( + "" if not adds else f"+{''.join(adds)}", + "" if not removes else f"-{''.join(removes)}", + args + ) + KICK_REASON_SETTING = utils.Setting("default-kick-reason", "Set the default kick reason", example="have a nice trip") @@ -33,10 +55,6 @@ BAN_FORMATTING = "${n} = nick, ${u} = username, ${h} = hostname, ${a} = account" @utils.export("channelset", utils.Setting("ban-format", "Set ban format (%s)" % BAN_FORMATTING, example="*!${u}@${h}")) -@utils.export("channelset", utils.Setting("ban-format-account", - "Set ban format for users with accounts (%s)" % BAN_FORMATTING, - example="~a:${a}")) - @utils.export("serverset", utils.OptionsSetting( list(QUIET_METHODS.keys()), "quiet-method", "Set this server's method of muting users")) @@ -47,6 +65,10 @@ BAN_FORMATTING = "${n} = nick, ${u} = username, ${h} = hostname, ${a} = account" @utils.export("botset", KICK_REASON_SETTING) @utils.export("serverset", KICK_REASON_SETTING) @utils.export("channelset", KICK_REASON_SETTING) + +@utils.export("channelset", utils.FunctionSetting(_mlock, "mlock", + "Set which modes are locked on and off for the current channel", + example="+mnt-z")) class Module(ModuleManager.BaseModule): _name = "ChanOp" @@ -75,23 +97,6 @@ class Module(ModuleManager.BaseModule): reason = reason or self._kick_reason(server, channel) channel.send_kicks(nicknames, reason) - def _format_hostmask(self, user, s): - vars = {} - vars["n"] = vars["nickname"] = user.nickname - vars["u"] = vars["username"] = user.username - vars["h"] = vars["hostname"] = user.hostname - vars["a"] = vars["account"] = user.account or "" - return utils.parse.format_token_replace(s, vars) - def _get_hostmask(self, channel, user): - if not user.account == None: - account_format = channel.get_setting("ban-format-account", None) - if not account_format == None: - return self._format_hostmask(user, account_format) - - format = channel.get_setting("ban-format", "*!${u}@${h}") - return self._format_hostmask(user, format) - - @utils.hook("received.command.topic") @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "low,topic") @@ -209,7 +214,8 @@ class Module(ModuleManager.BaseModule): elif flag == "V" and identified: modes.append(("v", user.nickname)) elif flag == "b": - modes.append(("b", self._get_hostmask(channel, user))) + mask = self.exports.get("ban-mask")(server, channel, user) + modes.append(("b", mask)) kick_reason = "User is banned from this channel" new_modes = [] @@ -394,15 +400,16 @@ class Module(ModuleManager.BaseModule): elif spec[2][0] in ["user", "cuser"]: users = [spec[2][1]] elif spec[2][0] == "word": - masks = [spec[2][1]] + args = [spec[2][1]] target_type, mode, prefix = self._find_mode(type, server) if users: if target_type == TargetType.MASK: - args = [self._get_hostmask(spec[0], u) for u in users] + mask_f = self.exports.get("ban-mask") + args = [mask_f(server, spec[0], u) for u in users] elif target_type == TargetType.NICKNAME: args = [ - u.nickname for u in users if not spec[0].has_mode(u, mode)] + u.nickname for u in users if not spec[0].has_umode(u, mode)] elif target_type == TargetType.ACCOUNT: args = [u.account for u in users if not u.account == None] @@ -428,7 +435,9 @@ class Module(ModuleManager.BaseModule): users = args = [] if event["spec"][1][0] == "user": - masks = [self._get_hostmask(event["spec"][0], event["spec"][1][1])] + mask_f = self.exports.get("ban-mask") + masks = [ + mask_f(event["server"], event["spec"][0], event["spec"][1][1])] elif event["spec"][1][0] == "word": masks = self._list_query_event(event["spec"][0], event["spec"][1][1], mode, prefix) @@ -457,7 +466,7 @@ class Module(ModuleManager.BaseModule): _, mode, _ = self._find_mode( event["hook"].get_kwarg("type"), event["server"]) valid_nicks = [ - u.nickname for u in users if event["spec"][0].has_mode(u, mode)] + u.nickname for u in users if event["spec"][0].has_umode(u, mode)] if valid_nicks: event["spec"][0].send_modes([(mode, a) for a in valid_nicks], False) @@ -489,3 +498,43 @@ class Module(ModuleManager.BaseModule): if modes: event["spec"][0].send_modes(modes, False) + + @utils.hook("received.324") + @utils.hook("received.mode.channel") + def on_modes(self, event): + mlock = event["channel"].get_setting("mlock", None) + if mlock: + changes_adds = "" + changes_removes = "" + changes_args = [] + adds, removes, args = mlock + args = args.copy() # cached settings objects are mutable + + for mode in adds[1:]: + if not event["channel"].has_mode(mode): + if (mode in event["server"].channel_list_modes or + mode in event["server"].channel_parametered_modes or + mode in event["server"].channel_setting_modes): + changes_adds += mode + changes_args.append(args.pop(0)) + elif mode in event["server"].channel_modes: + changes_adds += mode + + for mode in removes[1:]: + if event["channel"].has_mode(mode): + if (mode in event["server"].channel_list_modes or + mode in event["server"].channel_parametered_modes): + changes_removes += mode + changes_args.append(args.pop(0)) + elif (mode in event["server"].channel_setting_modes or + mode in event["server"].channel_modes): + changes_removes += mode + + out = "" + if changes_adds: + out += f"+{changes_adds}" + if changes_removes: + out += f"-{changes_removes}" + + if out: + event["channel"].send_mode(out, changes_args) diff --git a/modules/dnsbl/__init__.py b/modules/dnsbl/__init__.py index 495cbd8c..2b3daf35 100644 --- a/modules/dnsbl/__init__.py +++ b/modules/dnsbl/__init__.py @@ -14,11 +14,11 @@ class Module(ModuleManager.BaseModule): lists = [] for i, arg in reversed(list(enumerate(args))): if arg[0] == "@": - hostname = args.pop(i) + hostname = args.pop(i)[1:] if hostname in default_lists: lists.insert(0, default_lists[hostname]) else: - lists.insert(0, lists.DNSBL(hostname)) + lists.insert(0, _lists.DNSBL(hostname)) lists = lists or list(default_lists.values()) diff --git a/modules/dnsbl/lists.py b/modules/dnsbl/lists.py index ce2b6404..b84628ea 100644 --- a/modules/dnsbl/lists.py +++ b/modules/dnsbl/lists.py @@ -6,7 +6,7 @@ class DNSBL(object): self.hostname = hostname def process(self, result: str): - return "unknown" + return result class ZenSpamhaus(DNSBL): hostname = "zen.spamhaus.org" @@ -40,10 +40,18 @@ class DroneBL(DNSBL): elif result in ["12", "13", "15", "16"]: return "exploits" +class AbuseAtCBL(DNSBL): + hostname = "cbl.abuseat.org" + def process(self, result): + result = result.rsplit(".", 1)[1] + if result == "2": + return "abuse" + DEFAULT_LISTS = [ ZenSpamhaus(), EFNetRBL(), - DroneBL() + DroneBL(), + AbuseAtCBL() ] def default_lists(): diff --git a/modules/fediverse/__init__.py b/modules/fediverse/__init__.py index c1279eb7..aefd966e 100644 --- a/modules/fediverse/__init__.py +++ b/modules/fediverse/__init__.py @@ -95,7 +95,7 @@ class Module(ModuleManager.BaseModule): note = note["object"] cw, author, content, url = ap_utils.parse_note(actor, note, type) - shorturl = self.exports.get_one("shorturl")(event["server"], url, + shorturl = self.exports.get("shorturl")(event["server"], url, context=event["target"]) if cw: diff --git a/modules/fediverse/ap_server.py b/modules/fediverse/ap_server.py index e1b5cece..1fb98760 100644 --- a/modules/fediverse/ap_server.py +++ b/modules/fediverse/ap_server.py @@ -12,7 +12,7 @@ class Server(object): self.username = username self.instance = instance - url_for = self.exports.get_one("url-for") + url_for = self.exports.get("url-for") key_id = self._ap_keyid_url(url_for) private_key = ap_security.PrivateKey(self.bot.config["tls-key"], key_id) @@ -61,7 +61,7 @@ class Server(object): def _toot(self, activity_id): content, timestamp = self.bot.get_setting( "ap-activity-%s" % activity_id) - url_for = self.exports.get_one("url-for") + url_for = self.exports.get("url-for") self_id = self._ap_self_url(url_for) activity_url = self._ap_activity_url(url_for, activity_id) diff --git a/modules/fediverse/ap_utils.py b/modules/fediverse/ap_utils.py index 0d44d523..9174648c 100644 --- a/modules/fediverse/ap_utils.py +++ b/modules/fediverse/ap_utils.py @@ -39,15 +39,17 @@ class FindActorException(Exception): pass def find_actor(username, instance): - hostmeta = HOSTMETA_TEMPLATE % instance - hostmeta_request = utils.http.Request(HOSTMETA_TEMPLATE % instance) + hostmeta_url = HOSTMETA_TEMPLATE % instance + hostmeta_request = utils.http.Request(hostmeta_url) try: hostmeta = utils.http.request(hostmeta_request) except: - raise FindActorException("Failed to get host-meta for %s" % instance) + # failed to GET hostmeta; this is an optional step for servers that do + # not host their webfinger at the usual URL (see WEBFINGER_TEMPLATE) + hostmeta = None webfinger_url = None - if hostmeta.code == 200: + if hostmeta and hostmeta.code == 200: for item in hostmeta.soup().find_all("link"): if item["rel"] and item["rel"][0] == "lrdd": webfinger_url = item["template"] @@ -60,8 +62,9 @@ def find_actor(username, instance): try: webfinger = activity_request(webfinger_url, type=JRD_TYPE) - except: - raise FindActorException("Failed to get webfinger for %s" % instance) + except Exception as e: + raise FindActorException("Failed to get webfinger for %s: %s" % + (instance, str(e))) actor_url = None if webfinger.code == 200: diff --git a/modules/git_webhooks/__init__.py b/modules/git_webhooks/__init__.py index b87f0665..44d9b5dc 100644 --- a/modules/git_webhooks/__init__.py +++ b/modules/git_webhooks/__init__.py @@ -138,7 +138,7 @@ class Module(ModuleManager.BaseModule): if url: if channel.get_setting("git-shorten-urls", False): - url = self.exports.get_one("shorturl")(server, url, + url = self.exports.get("shorturl")(server, url, context=channel) or url output = "%s - %s" % (output, url) diff --git a/modules/git_webhooks/github.py b/modules/git_webhooks/github.py index 21b672ce..8bf61114 100644 --- a/modules/git_webhooks/github.py +++ b/modules/git_webhooks/github.py @@ -441,7 +441,7 @@ class GitHub(object): url = "" if data["check_run"]["details_url"]: url = data["check_run"]["details_url"] - url = " - %s" % self.exports.get_one("shorturl-any")(url) + url = " - %s" % self.exports.get("shorturl-any")(url) duration = "" if data["check_run"]["completed_at"]: diff --git a/modules/ids.py b/modules/ids.py index 1738c549..2e056892 100644 --- a/modules/ids.py +++ b/modules/ids.py @@ -16,7 +16,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("help", "Show what I think your account name is") def account(self, event): event["stdout"].write("%s: %s" % (event["user"].nickname, - self.exports.get_one("account-name")(event["user"]))) + self.exports.get("account-name")(event["user"]))) @utils.hook("received.command.channelid", channel_only=True) def channel_id(self, event): diff --git a/modules/inactive_channels.py b/modules/inactive_channels.py index 5959d432..78f724c7 100644 --- a/modules/inactive_channels.py +++ b/modules/inactive_channels.py @@ -1,21 +1,23 @@ import datetime from src import ModuleManager, utils -PRUNE_TIMEDELTA = datetime.timedelta(weeks=2) +PRUNE_TIMEDELTA = datetime.timedelta(weeks=4) -SETTING_NAME = "inactive-channels" -SETTING = utils.BoolSetting(SETTING_NAME, - "Whether or not to leave inactive channels after 2 weeks") +SETTING_NAME = "inactive-prune" +SETTING = utils.IntRangeSetting(0, None, SETTING_NAME, + "Amount of days of inactivity before we leave a channel") -MODE_SETTING_NAME = "inactive-channel-modes" +MODE_SETTING_NAME = "inactive-prune-modes" MODE_SETTING = utils.BoolSetting(MODE_SETTING_NAME, "Whether or not we will leave inactive channels that we have a mode in") @utils.export("botset", SETTING) @utils.export("serverset", SETTING) -@utils.export("channelset", SETTING) @utils.export("serverset", MODE_SETTING) @utils.export("channelset", MODE_SETTING) + +@utils.export("channelset", utils.BoolSetting(SETTING_NAME, + "Whether or not to leave this channel when it is inactive")) class Module(ModuleManager.BaseModule): def _get_timestamp(self, channel): return channel.get_setting("last-message", None) @@ -35,29 +37,27 @@ class Module(ModuleManager.BaseModule): def hourly(self, event): parts = [] now = utils.datetime.utcnow() - botwide_setting = self.bot.get_setting(SETTING_NAME, False) + botwide_days = self.bot.get_setting(SETTING_NAME, None) botwide_mode_setting = self.bot.get_setting(MODE_SETTING_NAME, False) for server in self.bot.servers.values(): - serverwide_setting = server.get_setting( - SETTING_NAME, botwide_setting) - if not serverwide_setting: + serverwide_days = server.get_setting(SETTING_NAME, botwide_days) + if serverwide_days == None: continue mode_setting = server.get_setting( MODE_SETTING_NAME, botwide_mode_setting) - our_user = server.get_user(server.nickname) + for channel in server.channels: - if not channel.get_setting(SETTING_NAME, serverwide_setting): - continue - if not mode_setting and channel.get_user_modes(our_user): + if (not channel.get_setting(SETTING_NAME, True) or + not mode_setting and channel.get_user_modes(our_user)): continue timestamp = self._get_timestamp(channel) if timestamp: dt = utils.datetime.parse.iso8601(timestamp) - if (now-dt) >= PRUNE_TIMEDELTA: + if (now-dt).days >= serverwide_days: parts.append([server, channel]) for server, channel in parts: @@ -66,6 +66,7 @@ class Module(ModuleManager.BaseModule): channel.send_part("Channel inactive") self._del_timestamp(channel) + @utils.hook("send.message.channel") @utils.hook("received.message.channel") def channel_message(self, event): self._set_timestamp(event["channel"]) diff --git a/modules/ircv3_editmsg.py b/modules/ircv3_editmsg.py index a7b5b2b0..15163053 100644 --- a/modules/ircv3_editmsg.py +++ b/modules/ircv3_editmsg.py @@ -18,7 +18,7 @@ class Module(ModuleManager.BaseModule): timestamp, line.message) line = "- %s" % minimal - self.exports.get_one("format")("delete", event["server"], line, + self.exports.get("format")("delete", event["server"], line, event["target_str"], minimal=minimal, channel=channel, user=event["user"]) diff --git a/modules/ircv3_typing.py b/modules/ircv3_typing.py index 3c8c9c6d..05de5162 100644 --- a/modules/ircv3_typing.py +++ b/modules/ircv3_typing.py @@ -5,7 +5,10 @@ CAP = utils.irc.Capability("message-tags", "draft/message-tags-0.2") class Module(ModuleManager.BaseModule): def _tagmsg(self, target, state): return IRCLine.ParsedLine("TAGMSG", [target], - tags={"+draft/typing": state}) + tags={ + "+typing": state, + "+draft/typing": state + }) def _has_tags(self, server): return server.has_capability(CAP) diff --git a/modules/lastfm.py b/modules/lastfm.py index 14e9128a..6931e0c0 100644 --- a/modules/lastfm.py +++ b/modules/lastfm.py @@ -7,8 +7,13 @@ from src import ModuleManager, utils URL_SCROBBLER = "http://ws.audioscrobbler.com/2.0/" +SETTING_YT = utils.BoolSetting("lastfm-youtube", + "Whether or not to search last.fm now-playing results on youtube") + @utils.export("set", utils.Setting("lastfm", "Set last.fm username", example="jesopo")) +@utils.export("botset", SETTING_YT) +@utils.export("serverset", SETTING_YT) class Module(ModuleManager.BaseModule): _name = "last.fm" @@ -59,11 +64,13 @@ class Module(ModuleManager.BaseModule): time_language = "is listening to" if np else "last listened to" - yt_url = self.exports.get_one("search-youtube")( - "%s - %s" % (artist, track_name)) yt_url_str = "" - if yt_url: - yt_url_str = " - %s" % yt_url + if event["server"].get_setting("lastfm-youtube", + self.bot.get_setting("lastfm-youtube", False)): + yt_url = self.exports.get("search-youtube")( + "%s - %s" % (artist, track_name)) + if yt_url: + yt_url_str = " - %s" % yt_url info_page = utils.http.request(URL_SCROBBLER, get_params={ "method": "track.getInfo", "artist": artist, diff --git a/modules/message_filter.py b/modules/message_filter.py index a6191a66..f773ad36 100644 --- a/modules/message_filter.py +++ b/modules/message_filter.py @@ -31,20 +31,23 @@ class Module(ModuleManager.BaseModule): filters = self._get_filters(event["server"], target) for filter in filters: sed = utils.parse.sed.parse(filter) - type, out = utils.parse.sed.sed(sed, message) - if type == "m" and out: - self.log.info("Message matched filter, dropping: %s" - % event["line"].format()) - event["line"].invalidate() - return - elif type == "s": + if sed.type == "m": + out = utils.parse.sed.sed(sed, message_plain) + if out: + self.log.info("Message matched filter, dropping: %s" + % event["line"].format()) + event["line"].invalidate() + return + elif sed.type == "s": + out = utils.parse.sed.sed(sed, message) message = out if not message == original_message: event["line"].args[1] = message - @utils.hook("received.command.cfilter", channel_only=True) + @utils.hook("received.command.cfilter", channel_only=True, + require_access="high,filter", require_mode="o") @utils.hook("received.command.filter") @utils.hook("received.command.bfilter") @utils.kwarg("help", "Add a message filter for the current channel") diff --git a/modules/quotes.py b/modules/quotes.py index a843bdfb..e323cc9b 100644 --- a/modules/quotes.py +++ b/modules/quotes.py @@ -63,22 +63,30 @@ class Module(ModuleManager.BaseModule): category) found_target = None + found_quote = None if not remove_quote == None: - remove_quote_lower = remove_quote.lower() + remove_quote_lower = remove_quote.lower().strip() for nickname, time_added, quote, target in quotes[:]: - if quote.lower() == remove_quote_lower: - quotes.remove([nickname, time_added, quote]) + if remove_quote_lower in quote.lower().strip(): found_target = target + found_quote = [nickname, time_added, quote] message = "Removed quote from '%s'" break else: if quotes: - quote = quotes.pop(-1) - found_target = quote[-1] + nickname, time_added, quote, target = quotes.pop(-1) + + found_target = target + found_quote = [nickname, time_added, quote] message = "Removed last '%s' quote" if not message == None: - self._set_quotes(found_target, category, quotes) + target_quotes = self._get_quotes(found_target, category) + target_quotes.remove(found_quote) + self._set_quotes(found_target, category, target_quotes) + + _, _, quote = found_quote + message = f"{message} ({quote})" event["stdout"].write(message % category) else: event["stderr"].write("Quote not found") @@ -89,6 +97,10 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("usage", "<category> [= <search>]") def quote(self, event): category, search = self.category_and_quote(event["args"]) + if event["server"].has_user(category): + category = event["server"].get_user_nickname( + event["server"].get_user(category).get_id()) + quotes = event["server"].get_setting("quotes-%s" % category, []) if event["is_channel"]: quotes += self._get_quotes(event["target"], category) @@ -122,28 +134,26 @@ class Module(ModuleManager.BaseModule): raise utils.EventError( "Please provide a number between 1 and 3") - target = event["args_split"][0] - lines = event["target"].buffer.find_many_from(target, line_count) + target_user = event["args_split"][0] + lines = event["target"].buffer.find_many_from(target_user, line_count) if lines: lines.reverse() target = event["server"] if event["target"].get_setting("channel-quotes", False): target = event["target"] - quotes = self._get_quotes(target, target) - lines_str = [] for line in lines: lines_str.append(line.format()) text = " ".join(lines_str) - quotes.append([event["user"].name, int(time.time()), text]) - quote_category = line.sender if event["server"].has_user(quote_category): - account = event["server"].get_user_nickname( + quote_category = event["server"].get_user_nickname( event["server"].get_user(quote_category).get_id()) + quotes = self._get_quotes(target, quote_category) + quotes.append([event["user"].name, int(time.time()), text]) self._set_quotes(target, quote_category, quotes) event["stdout"].write("Quote added") diff --git a/modules/rss.py b/modules/rss.py index 07916861..9ba23db4 100644 --- a/modules/rss.py +++ b/modules/rss.py @@ -7,10 +7,15 @@ import feedparser RSS_INTERVAL = 60 # 1 minute +SETTING_BIND = utils.Setting("rss-bindhost", + "Which local address to bind to for RSS requests", example="127.0.0.1") + @utils.export("botset", utils.IntSetting("rss-interval", "Interval (in seconds) between RSS polls", example="120")) @utils.export("channelset", utils.BoolSetting("rss-shorten", "Whether or not to shorten RSS urls")) +@utils.export("serverset", SETTING_BIND) +@utils.export("channelset", SETTING_BIND) class Module(ModuleManager.BaseModule): _name = "RSS" def on_load(self): @@ -27,7 +32,7 @@ class Module(ModuleManager.BaseModule): link = entry.get("link", None) if shorten: try: - link = self.exports.get_one("shorturl")(server, link) + link = self.exports.get("shorturl")(server, link) except: pass link = " - %s" % link if link else "" @@ -49,26 +54,36 @@ class Module(ModuleManager.BaseModule): if server and channel_name in server.channels: channel = server.channels.get(channel_name) for url in urls: - if not url in hooks: - hooks[url] = [] - hooks[url].append((server, channel)) + bindhost = channel.get_setting("rss-bindhost", + server.get_setting("rss-bindhost", None)) + + if url.startswith("www."): + url = url.replace("www.", "", 1) + + key = (url, bindhost) + if not key in hooks: + hooks[key] = [] + + hooks[key].append((server, channel)) if not hooks: return requests = [] - for url in hooks.keys(): - requests.append(utils.http.Request(url, id=url)) + for url, bindhost in hooks.keys(): + requests.append(utils.http.Request(url, id=f"{url} {bindhost}", + bindhost=bindhost)) pages = utils.http.request_many(requests) - for url, channels in hooks.items(): - if not url in pages: + for (url, bindhost), channels in hooks.items(): + key = f"{url} {bindhost}" + if not key in pages: # async url get failed continue try: - data = pages[url].decode() + data = pages[key].decode() except Exception as e: self.log.error("Failed to decode rss URL %s", [url], exc_info=True) diff --git a/modules/seen.py b/modules/seen.py index b19dce61..7eb037d8 100644 --- a/modules/seen.py +++ b/modules/seen.py @@ -36,7 +36,7 @@ class Module(ModuleManager.BaseModule): since = utils.datetime.format.to_pretty_since( time.time()-seen_seconds, max_units=2) event["stdout"].write("%s was last seen %s ago%s" % ( - event["args_split"][0], since, seen_info or "")) + user.nickname, since, seen_info or "")) else: event["stderr"].write("I have never seen %s before." % ( - event["args_split"][0])) + user.nickname)) diff --git a/modules/shorturl.py b/modules/shorturl.py index afc1b50c..345d2183 100644 --- a/modules/shorturl.py +++ b/modules/shorturl.py @@ -16,36 +16,48 @@ class Module(ModuleManager.BaseModule): self.exports.add("botset", setting) def _shorturl_options_factory(self): - shorteners = self.exports.find("shorturl-s-") - return [s.replace("shorturl-s-", "", 1) for s in shorteners] + shorteners = set(self.exports.find("shorturl-s-")) + shorteners.update(self.exports.find("shorturl-x-")) + return sorted(s.split("-", 2)[-1] for s in shorteners) def _get_shortener(self, name): - return self.exports.get_one("shorturl-s-%s" % name, None) - def _call_shortener(self, shortener_name, url): - shortener = self._get_shortener(shortener_name) + extended = self.exports.get("shorturl-x-%s" % name, None) + if not extended == None: + return True, extended + return False, self.exports.get("shorturl-s-%s" % name, None) + def _call_shortener(self, server, context, shortener_name, url): + extended, shortener = self._get_shortener(shortener_name) if shortener == None: return None - short_url = shortener(url) + + if extended: + short_url = shortener(server, context, url) + else: + short_url = shortener(url) + if short_url == None: return None return short_url @utils.export("shorturl-any") def _shorturl_any(self, url): - return self._call_shortener("bitly", url) or url + return self._call_shortener(server, None, "bitly", url) or url @utils.export("shorturl") def _shorturl(self, server, url, context=None): shortener_name = None if context: shortener_name = context.get_setting("url-shortener", - server.get_setting("url-shortener", "bitly")) + server.get_setting("url-shortener", + self.bot.get_setting("url-shortener", "bitly"))) else: - shortener_name = server.get_setting("url-shortener", "bitly") + shortener_name = server.get_setting("url-shortener", + self.bot.get_setting("url-shortener", "bitly")) if shortener_name == None: return url - return self._call_shortener(shortener_name, url) or url + return self._call_shortener( + server, context, shortener_name, url) or url @utils.export("shorturl-s-bitly") def _bitly(self, url): diff --git a/modules/title.py b/modules/title.py index df05546e..eba18566 100644 --- a/modules/title.py +++ b/modules/title.py @@ -27,7 +27,7 @@ class Module(ModuleManager.BaseModule): for title_word in RE_WORDSPLIT.split(title): if len(title_word) > 1 or title_word.isalpha(): title_word = title_word.lower() - title_words.append(title_word.strip("'\"<>()")) + title_words.append(title_word.strip("'\"<>(),:")) if title_words: present = 0 @@ -45,13 +45,9 @@ class Module(ModuleManager.BaseModule): if not urllib.parse.urlparse(url).scheme: url = "http://%s" % url - hostname = urllib.parse.urlparse(url).hostname - if not utils.http.host_permitted(hostname): - self.log.warn("Attempted to get forbidden host: %s", [url]) - return -1, None - + request = utils.http.Request(url, check_hostname=True) try: - page = utils.http.request(url) + page = utils.http.request(request) except Exception as e: self.log.error("failed to get URL title for %s: %s", [url, str(e)]) return -1, None @@ -71,7 +67,7 @@ class Module(ModuleManager.BaseModule): return -2, title if channel.get_setting("title-shorten", False): - short_url = self.exports.get_one("shorturl")(server, url, + short_url = self.exports.get("shorturl")(server, url, context=channel) return page.code, "%s - %s" % (title, short_url) return page.code, title diff --git a/modules/tweets/format.py b/modules/tweets/format.py index 540e7638..9648dc51 100644 --- a/modules/tweets/format.py +++ b/modules/tweets/format.py @@ -23,7 +23,7 @@ def _tweet(exports, server, tweet, from_url): short_url = "" if not from_url: - short_url = exports.get_one("shorturl")(server, tweet_link) + short_url = exports.get("shorturl")(server, tweet_link) short_url = " - %s" % short_url if short_url else "" created_at = _timestamp(tweet.created_at) diff --git a/modules/user_time.py b/modules/user_time.py index 762a9440..8d0701e6 100644 --- a/modules/user_time.py +++ b/modules/user_time.py @@ -34,7 +34,7 @@ class Module(ModuleManager.BaseModule): location["timezone"]) if query: - location = self.exports.get_one("get-location")(query) + location = self.exports.get("get-location")(query) if location: return (LocationType.NAME, location["name"], location["timezone"]) diff --git a/modules/weather.py b/modules/weather.py index 1677f990..bd6b5e8b 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -45,7 +45,7 @@ class Module(ModuleManager.BaseModule): if location == None and query: - location_info = self.exports.get_one("get-location")(query) + location_info = self.exports.get("get-location")(query) if not location_info == None: location = [location_info["lat"], location_info["lon"], location_info.get("name", None)] @@ -74,8 +74,8 @@ class Module(ModuleManager.BaseModule): # wind speed is in metres per second - 3.6* for KMh wind_speed = 3.6*page["wind"]["speed"] - wind_speed_k = "%sKMh" % round(wind_speed, 1) - wind_speed_m = "%sMPh" % round(0.6214*wind_speed, 1) + wind_speed_k = "%skm/h" % round(wind_speed, 1) + wind_speed_m = "%smi/h" % round(0.6214*wind_speed, 1) if not nickname == None: location_str = "(%s) %s" % (nickname, location_str) diff --git a/modules/words.py b/modules/words.py index 88dbc6cc..59b9ef92 100644 --- a/modules/words.py +++ b/modules/words.py @@ -66,7 +66,7 @@ class Module(ModuleManager.BaseModule): if event["channel"].get_setting("word-tracking-registered", event["server"].get_setting("word-tracking-registered", False)): - if not self.exports.get_one("is-identified")(event["user"]): + if not self.exports.get("is-identified")(event["user"]): return if user.get_setting("first-words", None) == None: @@ -105,19 +105,24 @@ class Module(ModuleManager.BaseModule): self._channel_message(event["server"].get_user( event["server"].nickname), event) - @utils.hook("received.command.words", channel_only=True) + @utils.hook("received.command.words") @utils.kwarg("help", "See how many words you or the given nickname have used") - @utils.spec("!-channelonly ?<nickname>ouser") + @utils.spec("?<nickname>ouser") def words(self, event): - target_user = event["spec"][0] or event["user"] + if event["spec"][0] and event["is_channel"]: + target_user = event["spec"][0] + else: + target_user = event["user"] - words = dict(self._user_all(target_user)) - this_channel = words.get(event["target"].id, 0) + word_items = self._user_all(target_user) - total = 0 - for channel_id in words: - total += words[channel_id] + words = {} + for channel_id, count in word_items: + if not channel_id in words: + words[channel_id] = 0 + words[channel_id] += count + total = sum(words.values()) since = "" first_words = target_user.get_setting("first-words", None) @@ -125,9 +130,14 @@ class Module(ModuleManager.BaseModule): since = " since %s" % utils.datetime.format.date_human( utils.datetime.timestamp(first_words)) - event["stdout"].write("%s has used %d words (%d in %s)%s" % ( - target_user.nickname, total, this_channel, event["target"].name, - since)) + if event["is_channel"]: + this_channel = words.get(event["target"].id, 0) + event["stdout"].write("%s has used %d words (%d in %s)%s" % ( + target_user.nickname, total, this_channel, event["target"].name, + since)) + else: + event["stdout"].write("%s has used %d words%s" % ( + target_user.nickname, total, since)) @utils.hook("received.command.trackword") @utils.kwarg("help", "Start tracking a word") @@ -193,7 +203,9 @@ class Module(ModuleManager.BaseModule): user_words = {} for user_id, word_count in words: _, nickname = self.bot.database.users.by_id(user_id) - user_words[nickname] = word_count + if not nickname in user_words: + user_words[nickname] = 0 + user_words[nickname] += word_count top_10 = utils.top_10(user_words, convert_key=lambda nickname: self._get_nickname( diff --git a/modules/yourls.py b/modules/yourls.py new file mode 100644 index 00000000..3d0fa6e1 --- /dev/null +++ b/modules/yourls.py @@ -0,0 +1,34 @@ +import urllib.parse +from src import ModuleManager, utils + +def _parse(s): + parsed = urllib.parse.urlparse(s) + return urllib.parse.urljoin(s, parsed.path), parsed.query + +SETTING = utils.FunctionSetting(_parse, "yourls", + "Set YOURLS server (and token) to use for URL shortening", + example="https://bitbot.dev/yourls-api.php?1002a612b4", + format=utils.sensitive_format) + +@utils.export("botset", SETTING) +@utils.export("serverset", SETTING) +@utils.export("channelset", SETTING) +class Module(ModuleManager.BaseModule): + @utils.export("shorturl-x-yourls") + def _shorturl(self, server, context, url): + setting = server.get_setting("yourls", + self.bot.get_setting("yourls", None)) + if context: + setting = context.get_setting("yourls", setting) + + if not setting == None: + shortener_url, token = setting + + page = utils.http.request(shortener_url, get_params={ + "signature": token, + "action": "shorturl", + "url": url, + "format": "json"}).json() + if page: + return page["shorturl"] + return None diff --git a/modules/youtube.py b/modules/youtube.py index f5710199..7699fdd6 100644 --- a/modules/youtube.py +++ b/modules/youtube.py @@ -24,6 +24,8 @@ ARROW_DOWN = "↓" "Turn safe search off/on")) class Module(ModuleManager.BaseModule): def get_video_page(self, video_id): + self.log.debug("youtube API request: " + "videos.list [contentDetails,snippet,statistics]") return utils.http.request(URL_YOUTUBEVIDEO, get_params={ "part": "contentDetails,snippet,statistics", "id": video_id, "key": self.bot.config["google-api-key"]}).json() @@ -76,7 +78,10 @@ class Module(ModuleManager.BaseModule): return None def get_playlist_page(self, playlist_id): - return utils.http.request(URL_YOUTUBEPLAYLIST, get_params={ + self.log.debug("youtube API request: " + "playlists.list [contentDetails,snippet]") + + return utils.http.request(URL_YOUTUBEPLAYLIST, get_params={ "part": "contentDetails,snippet", "id": playlist_id, "key": self.bot.config["google-api-key"]}).json() def playlist_details(self, playlist_id): @@ -109,6 +114,8 @@ class Module(ModuleManager.BaseModule): def _search_youtube(self, query): video_id = "" + self.log.debug("youtube API request: search.list (A) [snippet]") + search_page = utils.http.request(URL_YOUTUBESEARCH, get_params={"q": query, "part": "snippet", "maxResults": "1", "type": "video", @@ -144,6 +151,9 @@ class Module(ModuleManager.BaseModule): if not url: safe_setting = event["target"].get_setting("youtube-safesearch", True) safe = "moderate" if safe_setting else "none" + + self.log.debug("youtube API request: search.list (B) [snippet]") + search_page = utils.http.request(URL_YOUTUBESEARCH, get_params={"q": search, "part": "snippet", "maxResults": "1", "type": "video", "key": self.bot.config["google-api-key"], |
