diff options
Diffstat (limited to 'modules')
28 files changed, 503 insertions, 346 deletions
diff --git a/modules/admin.py b/modules/admin.py index 37d61384..765758ca 100644 --- a/modules/admin.py +++ b/modules/admin.py @@ -81,10 +81,11 @@ class Module(ModuleManager.BaseModule): id = self.bot.database.servers.get_by_alias(alias) if id == None: raise utils.EventError("Unknown server alias") + server = self.bot.get_server_by_id(id) server.disconnect() self.bot.disconnect(server) - event["stdout"].write("Disconnected from %s" % alias) + event["stdout"].write("Disconnected from %s" % str(server)) @utils.hook("received.command.shutdown") def shutdown(self, event): diff --git a/modules/ctcp.py b/modules/ctcp.py index 6c973356..58f8d7e5 100644 --- a/modules/ctcp.py +++ b/modules/ctcp.py @@ -8,23 +8,23 @@ from src import IRCBot, ModuleManager, utils "help": "Set whether I respond to CTCPs on this server", "validate": utils.bool_or_none, "example": "on"}) class Module(ModuleManager.BaseModule): - @utils.hook("received.ctcp.version.private") + @utils.hook("received.ctcp.request.version") def ctcp_version(self, event): default = "BitBot %s (%s)" % (IRCBot.VERSION, IRCBot.SOURCE) event["user"].send_ctcp_response("VERSION", self.bot.config.get("ctcp-version", default)) - @utils.hook("received.ctcp.source.private") + @utils.hook("received.ctcp.request.source") def ctcp_source(self, event): event["user"].send_ctcp_response("SOURCE", self.bot.config.get("ctcp-source", IRCBot.SOURCE)) - @utils.hook("received.ctcp.ping.private") + @utils.hook("received.ctcp.request.ping") def ctcp_ping(self, event): event["user"].send_ctcp_response("PING", event["message"]) - @utils.hook("received.ctcp.time.private") + @utils.hook("received.ctcp.request.time") def ctcp_time(self, event): event["user"].send_ctcp_response("TIME", datetime.datetime.now().strftime("%c")) diff --git a/modules/github/__init__.py b/modules/github/__init__.py index b2a382b3..63d774a8 100644 --- a/modules/github/__init__.py +++ b/modules/github/__init__.py @@ -485,7 +485,8 @@ class Module(ModuleManager.BaseModule): continue regex = re.compile(r"(.)\b(%s)(%s)" % ( - user.nickname[0], user.nickname[1:]), re.I) + re.escape(user.nickname[0]), re.escape(user.nickname[1:])), + re.I) s = regex.sub("\\1\\2\u200c\\3", s) return s diff --git a/modules/ip_addresses.py b/modules/ip_addresses.py index 07f067fb..88bd9a56 100644 --- a/modules/ip_addresses.py +++ b/modules/ip_addresses.py @@ -6,7 +6,7 @@ from src import ModuleManager, utils URL_GEOIP = "http://ip-api.com/json/%s" REGEX_IPv6 = r"(?:(?:[a-f0-9]{1,4}:){2,}|[a-f0-9:]*::)[a-f0-9:]*" REGEX_IPv4 = r"(?:\d{1,3}\.){3}\d{1,3}" -REGEX_IP = re.compile("%s|%s" % (REGEX_IPv4, REGEX_IPv6), re.I) +REGEX_IP = re.compile("(%s)|(%s)" % (REGEX_IPv4, REGEX_IPv6), re.I) class Module(ModuleManager.BaseModule): @utils.hook("received.command.dns", min_args=1) diff --git a/modules/ircv3_chathistory.py b/modules/ircv3_chathistory.py new file mode 100644 index 00000000..1607263c --- /dev/null +++ b/modules/ircv3_chathistory.py @@ -0,0 +1,29 @@ +#--depends-on ircv3_msgid + +from src import ModuleManager, utils + +TAG = utils.irc.MessageTag("msgid", "draft/msgid") + +class Module(ModuleManager.BaseModule): + @utils.hook("received.batch.end") + def batch_end(self, event): + if event["batch"].type == "chathistory": + target_name = event["batch"].args[0] + if target_name in event["server"].channels: + target = event["server"].channels.get(target_name) + else: + target = event["server"].get_user(target_name) + + last_msgid = target.get_setting("last-msgid", None) + if not last_msgid == None: + lines = event["batch"].get_lines() + stop_index = -1 + + for i, line in enumerate(lines): + msgid = TAG.get_value(line.tags) + if msgid == last_msgid: + stop_index = i + break + + if not stop_index == -1: + return lines[stop_index+1:] diff --git a/modules/ircv3_labeled_responses.py b/modules/ircv3_labeled_responses.py new file mode 100644 index 00000000..eee53c58 --- /dev/null +++ b/modules/ircv3_labeled_responses.py @@ -0,0 +1,49 @@ +import uuid +from src import ModuleManager, utils + +CAP = utils.irc.Capability(None, "draft/labeled-response-0.2") +TAG = utils.irc.MessageTag(None, "draft/label") + +CAP_TO_TAG = { + "draft/labeled-response-0.2": "draft/label" +} + +class Module(ModuleManager.BaseModule): + @utils.hook("new.server") + def new_server(self, event): + event["server"]._label_cache = {} + + @utils.hook("received.cap.ls") + @utils.hook("received.cap.new") + def on_cap(self, event): + if CAP.available(event["capabilities"]): + return CAP.copy() + + @utils.hook("preprocess.send") + def raw_send(self, event): + available_cap = event["server"].available_capability(CAP) + + if available_cap: + label = TAG.get_value(event["line"].tags) + if label == None: + tag_key = CAP_TO_TAG[available_cap] + label = str(uuid.uuid4()) + event["line"].tags[tag_key] = label + + event["server"]._label_cache[label] = event["line"] + + @utils.hook("raw.received") + def raw_recv(self, event): + if not event["line"].command == "BATCH": + label = TAG.get_value(event["line"].tags) + if not label == None: + self._recv(event["server"], label, event["line"]) + + @utils.hook("received.batch.end") + def batch_end(self, event): + if TAG.match(event["batch"].type): + self._recv(event["server"], event["batch"].identifier, None) + + def _recv(self, server, label, line): + cached_line = server._label_cache.pop(label) + # do something with the line! diff --git a/modules/message_tracking.py b/modules/ircv3_message_tracking.py index 3f4ad88c..3f4ad88c 100644 --- a/modules/message_tracking.py +++ b/modules/ircv3_message_tracking.py diff --git a/modules/metadata.py b/modules/ircv3_metadata.py index 03fcc45a..55193dce 100644 --- a/modules/metadata.py +++ b/modules/ircv3_metadata.py @@ -13,4 +13,5 @@ class Module(ModuleManager.BaseModule): def _ack(self, server): url = self.bot.get_setting("bot-url", IRCBot.SOURCE) - server.send_raw("METADATA * SET bot-url :%s" % url) + server.send_raw("METADATA * SET bot BitBot") + server.send_raw("METADATA * SET homepage :%s" % url) diff --git a/modules/ircv3_msgid.py b/modules/ircv3_msgid.py new file mode 100644 index 00000000..520198d6 --- /dev/null +++ b/modules/ircv3_msgid.py @@ -0,0 +1,21 @@ +from src import ModuleManager, utils + +TAG = utils.irc.MessageTag("msgid", "draft/msgid") + +class Module(ModuleManager.BaseModule): + def _on_channel(self, channel, tags): + msgid = TAG.get_value(tags) + if not msgid == None: + channel.set_setting("last-msgid", msgid) + + @utils.hook("received.message.channel") + @utils.hook("received.notice.channel") + @utils.hook("received.tagmsg.channel") + def on_channel(self, event): + self._on_channel(event["channel"], event["tags"]) + + @utils.hook("received.ctcp.request") + @utils.hook("received.ctcp.response") + def ctcp(self, event): + if event["is_channel"]: + self._on_channel(event["target"], event["tags"]) diff --git a/modules/resume.py b/modules/ircv3_resume.py index c3283bde..3184aa35 100644 --- a/modules/resume.py +++ b/modules/ircv3_resume.py @@ -1,3 +1,5 @@ +#--depends-on ircv3_server_time + from src import ModuleManager, utils CAP = utils.irc.Capability(None, "draft/resume-0.4") @@ -12,13 +14,13 @@ class Module(ModuleManager.BaseModule): def _del_token(self, server, new=False): server.del_setting(self._setting(new)) - def _get_timestamp(self, server): - return server.get_setting("last-read", None) @utils.hook("new.server") def new_server(self, event): - resume_timestamp = self._get_timestamp(event["server"]) - event["server"]._resume_timestamp = resume_timestamp + # we need to pull this before any data has been exchanged - to make sure + # it's not overwritten from the last connection + event["server"]._resume_timestamp = event["server"].get_setting( + "last-server-time", None) @utils.hook("received.cap.ls") def on_cap_ls(self, event): diff --git a/modules/sasl/README.md b/modules/ircv3_sasl/README.md index 30a51e08..30a51e08 100644 --- a/modules/sasl/README.md +++ b/modules/ircv3_sasl/README.md diff --git a/modules/sasl/__init__.py b/modules/ircv3_sasl/__init__.py index b62309a6..b62309a6 100644 --- a/modules/sasl/__init__.py +++ b/modules/ircv3_sasl/__init__.py diff --git a/modules/sasl/scram.py b/modules/ircv3_sasl/scram.py index f243d1e6..f243d1e6 100644 --- a/modules/sasl/scram.py +++ b/modules/ircv3_sasl/scram.py diff --git a/modules/ircv3_server_time.py b/modules/ircv3_server_time.py new file mode 100644 index 00000000..e363b341 --- /dev/null +++ b/modules/ircv3_server_time.py @@ -0,0 +1,17 @@ +from src import ModuleManager, utils + +CAP = utils.irc.Capability("server-time") +TAG = utils.irc.MessageTag("time") + +class Module(ModuleManager.BaseModule): + @utils.hook("received.cap.ls") + @utils.hook("received.cap.new") + def on_cap(self, event): + if CAP.available(event["capabilities"]): + return CAP.copy() + + @utils.hook("raw.received") + def raw_recv(self, event): + server_time = TAG.get_value(event["line"].tags) + if not server_time == None: + event["server"].set_setting("last-server-time", server_time) diff --git a/modules/sts.py b/modules/ircv3_sts.py index 09ecf523..09ecf523 100644 --- a/modules/sts.py +++ b/modules/ircv3_sts.py diff --git a/modules/lastfm.py b/modules/lastfm.py index 19dd69a7..2e08bdab 100644 --- a/modules/lastfm.py +++ b/modules/lastfm.py @@ -20,13 +20,23 @@ class Module(ModuleManager.BaseModule): :help: Get the last listened to track from a user :usage: [username] """ - if event["args_split"]: - lastfm_username = event["args_split"][0] - shown_username = lastfm_username + user = None + lastfm_username = None + shown_username = None + + if event["args"]: + arg_username = event["args_split"][0] + if event["server"].has_user_id(arg_username): + user = event["server"].get_user(event["args_split"][0]) + else: + lastfm_username = shown_username = arg_username else: - lastfm_username = event["user"].get_setting("lastfm", - event["user"].nickname) - shown_username = event["user"].nickname + user = event["user"] + + if user: + lastfm_username = user.get_setting("lastfm", user.nickname) + shown_username = user.nickname + page = utils.http.request(URL_SCROBBLER, get_params={ "method": "user.getrecenttracks", "user": lastfm_username, "api_key": self.bot.config["lastfm-api-key"], diff --git a/modules/line_handler/__init__.py b/modules/line_handler/__init__.py index a11fdec5..19e1902b 100644 --- a/modules/line_handler/__init__.py +++ b/modules/line_handler/__init__.py @@ -2,11 +2,6 @@ import enum from src import EventManager, ModuleManager, utils from . import channel, core, ircv3, message, user -LABELED_BATCH = { - "labeled-response": "label", - "draft/labeled-response": "draft/label" -} - class Module(ModuleManager.BaseModule): def _handle(self, server, line): hooks = self.events.on("raw.received").on(line.command).get_hooks() @@ -15,8 +10,9 @@ class Module(ModuleManager.BaseModule): default_events.append(hook.kwargs.get("default_event", False)) default_event = any(default_events) - kwargs = {"args": line.args, "tags": line.tags, "server": server, - "prefix": line.prefix, "direction": utils.Direction.Recv} + kwargs = {"command": line.command, "args": line.args, "tags": line.tags, + "server": server, "source": line.source, + "direction": utils.Direction.Recv} self.events.on("raw.received").on(line.command).call_unsafe(**kwargs) if default_event or not hooks: @@ -26,7 +22,7 @@ class Module(ModuleManager.BaseModule): def handle_raw(self, event): if ("batch" in event["line"].tags and event["line"].tags["batch"] in event["server"].batches): - event["server"].batches[event["line"].tags["batch"]].lines.append( + event["server"].batches[event["line"].tags["batch"]].add_line( event["line"]) else: self._handle(event["server"], event["line"]) @@ -34,8 +30,9 @@ class Module(ModuleManager.BaseModule): @utils.hook("raw.send") def handle_send(self, event): self.events.on("raw.send").on(event["line"].command).call_unsafe( - args=event["line"].args, tags=event["line"].tags, - server=event["server"], direction=utils.Direction.Send) + command=event["line"].command, args=event["line"].args, + tags=event["line"].tags, server=event["server"], + direction=utils.Direction.Send) # ping from the server @utils.hook("raw.received.ping") @@ -158,22 +155,14 @@ class Module(ModuleManager.BaseModule): def invite(self, event): core.invite(self.events, event) - # we've received/sent a message + # we've received/sent a PRIVMSG, NOTICE or TAGMSG @utils.hook("raw.received.privmsg") @utils.hook("raw.send.privmsg") - def privmsg(self, event): - message.privmsg(self.events, event) - - # we've received/sent a notice @utils.hook("raw.received.notice") @utils.hook("raw.send.notice") - def notice(self, event): - message.notice(self.events, event) - - # IRCv3 TAGMSG, used to send tags without any other information @utils.hook("raw.received.tagmsg") - def tagmsg(self, event): - message.tagmsg(self.events, event) + def message(self, event): + message.message(self.events, event) # IRCv3 AWAY, used to notify us that a client we can see has changed /away @utils.hook("raw.received.away") @@ -187,20 +176,29 @@ class Module(ModuleManager.BaseModule): if modifier == "+": batch_type = event["args"][1] - event["server"].batches[identifier] = utils.irc.IRCRecvBatch( - identifier, batch_type, event["tags"]) + args = event["args"][2:] + + batch = utils.irc.IRCBatch(identifier, batch_type, args, + event["tags"]) + event["server"].batches[identifier] = batch + + self.events.on("received.batch.start").call(batch=batch, + server=event["server"]) else: batch = event["server"].batches[identifier] del event["server"].batches[identifier] - add_tags = {} - if batch.type in LABELED_BATCH.keys(): - tag_name = LABELED_BATCH[batch.type] - add_tags[tag_name] = batch.tags[tag_name] + lines = batch.get_lines() + + results = self.events.on("received.batch.end").call(batch=batch, + server=event["server"]) + + for result in results: + if not result == None: + lines = result + break - for line in batch.lines: - if add_tags: - line.tags.update(add_tags) + for line in lines: self._handle(event["server"], line) # IRCv3 CHGHOST, a user's username and/or hostname has changed diff --git a/modules/line_handler/channel.py b/modules/line_handler/channel.py index 5b877764..759fdf45 100644 --- a/modules/line_handler/channel.py +++ b/modules/line_handler/channel.py @@ -8,7 +8,7 @@ def handle_332(events, event): topic=topic) def topic(events, event): - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) channel = event["server"].channels.get(event["args"][0]) topic = event["args"].get(1) channel.set_topic(topic) @@ -69,17 +69,17 @@ def join(events, event): account = event["args"][1] realname = event["args"][2] - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) - user.username = event["prefix"].username - user.hostname = event["prefix"].hostname + user.username = event["source"].username + user.hostname = event["source"].hostname if account: user.identified_account = account user.identified_account_id = event["server"].get_user(account).get_id() if realname: user.realname = realname - is_self = event["server"].is_own_nickname(event["prefix"].nickname) + is_self = event["server"].is_own_nickname(event["source"].nickname) if is_self: channel = event["server"].channels.add(channel_name) else: @@ -101,7 +101,7 @@ def join(events, event): def part(events, event): channel = event["server"].channels.get(event["args"][0]) - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) reason = event["args"].get(1) channel.remove_user(user) @@ -109,7 +109,7 @@ def part(events, event): if not len(user.channels): event["server"].remove_user(user) - if not event["server"].is_own_nickname(event["prefix"].nickname): + if not event["server"].is_own_nickname(event["source"].nickname): events.on("received.part").call(channel=channel, reason=reason, user=user, server=event["server"]) else: @@ -137,7 +137,7 @@ def handle_477(timers, event): server_id=event["server"].id) def kick(events, event): - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) target = event["args"][1] channel = event["server"].channels.get(event["args"][0]) reason = event["args"].get(2) diff --git a/modules/line_handler/core.py b/modules/line_handler/core.py index 88544a08..3d656b2f 100644 --- a/modules/line_handler/core.py +++ b/modules/line_handler/core.py @@ -7,10 +7,11 @@ def ping(event): event["server"].send_pong(event["args"][0]) def handle_001(event): - event["server"].socket.set_write_throttling(True) - event["server"].name = event["prefix"].hostmask + event["server"].socket.enable_write_throttle() + event["server"].name = event["source"].hostmask event["server"].set_own_nickname(event["args"][0]) event["server"].send_whois(event["server"].nickname) + event["server"].connected = True def handle_005(events, event): isupport_list = event["args"][1:-1] @@ -64,7 +65,7 @@ def motd_line(event): event["server"].motd_lines.append(event["args"][1]) def mode(events, event): - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) target = event["args"][0] is_channel = target[0] in event["server"].channel_types if is_channel: @@ -97,7 +98,7 @@ def mode(events, event): def invite(events, event): target_channel = event["args"][1] - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) target_user = event["server"].get_user(event["args"][0]) events.on("received.invite").call(user=user, target_channel=target_channel, server=event["server"], target_user=target_user) diff --git a/modules/line_handler/ircv3.py b/modules/line_handler/ircv3.py index 42afece7..d9f83b75 100644 --- a/modules/line_handler/ircv3.py +++ b/modules/line_handler/ircv3.py @@ -10,11 +10,9 @@ CAPABILITIES = [ utils.irc.Capability("away-notify"), utils.irc.Capability("userhost-in-names"), utils.irc.Capability("message-tags", "draft/message-tags-0.2"), - utils.irc.Capability("server-time"), utils.irc.Capability("cap-notify"), utils.irc.Capability("batch"), utils.irc.Capability("echo-message"), - utils.irc.Capability(None, "draft/labeled-response"), utils.irc.Capability(None, "draft/rename"), utils.irc.Capability(None, "draft/setname") ] diff --git a/modules/line_handler/message.py b/modules/line_handler/message.py index 4fb507ca..9157af66 100644 --- a/modules/line_handler/message.py +++ b/modules/line_handler/message.py @@ -1,163 +1,111 @@ from src import utils -def _from_self(server, direction, prefix): +def _from_self(server, direction, source): if direction == utils.Direction.Send: if server.has_capability_str("echo-message"): return None else: return True else: - if prefix: - return server.is_own_nickname(prefix.nickname) + if source: + return server.is_own_nickname(source.nickname) else: return False -def privmsg(events, event): +def message(events, event): from_self = _from_self(event["server"], event["direction"], - event.get("prefix", None)) + event.get("source", None)) if from_self == None: return - user = None - if "prefix" in event and not from_self: - user = event["server"].get_user(event["prefix"].nickname) + direction = "send" if from_self else "received" - message = event["args"][1] target_str = event["args"][0] + message = None + if len(event["args"]) > 1: + message = event["args"][1] + + if not from_self and ( + not event["source"] or + not event["server"].name or + event["source"].hostmask == event["server"].name or + target_str == "*"): + if event["source"]: + event["server"].name = event["source"].hostmask + + events.on("received.server-notice").call(message=message, + message_split=message.split(" "), server=event["server"]) + return + + if from_self: + user = event["server"].get_user(event["server"].nickname) + else: + user = event["server"].get_user(event["source"].nickname) + # strip prefix_symbols from the start of target, for when people use # e.g. 'PRIVMSG +#channel :hi' which would send a message to only # voiced-or-above users target = target_str.lstrip("".join(event["server"].prefix_symbols.keys())) - channel = None + is_channel = False + if target[0] in event["server"].channel_types: + is_channel = True if not target in event["server"].channels: return - channel = event["server"].channels.get(target) - - action = False - event_type = "message" - ctcp_message = utils.irc.parse_ctcp(message) - if ctcp_message: - message = ctcp_message.message - if ctcp_message.command == "ACTION": - action = True - message = ctcp_message.message - else: - event_type = "ctcp.%s" % ctcp_message.command - - if user and "account" in event["tags"]: - user.identified_account = event["tags"]["account"] - user.identified_account_id = event["server"].get_user( - event["tags"]["account"]).get_id() - - kwargs = {"message": message, "message_split": message.split(" "), - "server": event["server"], "tags": event["tags"], - "action": action, "target_str": target_str} - - direction = "send" if from_self else "received" - context = "channel" if channel else "private" - hook = events.on(direction).on(event_type).on(context) - - user_nickname = None - if user: - user_nickname = None if from_self else user.nickname - - if channel: - hook.call(user=user, channel=channel, **kwargs) - channel.buffer.add_message(user_nickname, message, action, - event["tags"], user==None) - elif from_self: - # a message we've sent to a user - user = event["server"].get_user(target) - hook.call(user=user, **kwargs) - user.buffer.add_message(user_nickname, message, action, - event["tags"], True) - elif event["server"].is_own_nickname(target): - hook.call(user=user, **kwargs) - user.buffer.add_message(user_nickname, message, action, - event["tags"], False) - -def notice(events, event): - from_self = _from_self(event["server"], event["direction"], - event.get("prefix", None)) - if from_self == None: - return - - message = event["args"][1] - target = event["args"][0] - - if "prefix" in event and ( - not event["prefix"] or - not event["server"].name or - event["prefix"].hostmask == event["server"].name or - target == "*"): - if event["prefix"]: - event["server"].name = event["prefix"].hostmask - - events.on("received.server-notice").call(message=message, - message_split=message.split(" "), server=event["server"]) + target_obj = event["server"].channels.get(target) else: - user = None - if "prefix" in event and not from_self: - user = event["server"].get_user(event["prefix"].nickname) - - channel = None - if target[0] in event["server"].channel_types: - channel = event["server"].channels.get(target) + target_obj = event["server"].get_user(target) - direction = "send" if from_self else "received" - context = "channel" if channel else "private" - hook = events.on(direction).on("notice").on(context) + kwargs = {"server": event["server"], "target": target_obj, + "target_str": target_str, "user": user, "tags": event["tags"], + "is_channel": is_channel} - user_nickname = None - if user: - user_nickname = None if from_self else user.nickname + action = False - kwargs = {"message": message, "message_split": message.split(" "), - "server": event["server"], "tags": event["tags"]} + if message: + ctcp_message = utils.irc.parse_ctcp(message) - if channel: - hook.call(user=user, channel=channel, **kwargs) - channel.buffer.add_notice(user_nickname, message, event["tags"], - user==None) - elif from_self: - # a notice we've sent to a user - user = event["server"].get_user(target) - hook.call(user=user, **kwargs) - user.buffer.add_notice(user_nickname, message, event["tags"], - True) - elif event["server"].is_own_nickname(target): - hook.call(user=user, **kwargs) - user.buffer.add_notice(user_nickname, message, event["tags"], - False) + if ctcp_message: + if not ctcp_message.command == "ACTION" or not event["command" + ] == "PRIVMSG": + if event["command"] == "PRIVMSG": + ctcp_action = "request" + else: + ctcp_action = "response" + events.on(direction).on("ctcp").on(ctcp_action).call( + message=ctcp_message.message, **kwargs) + events.on(direction).on("ctcp").on(ctcp_action).on( + ctcp_message.command).call(message=ctcp_message.message, + **kwargs) + return + else: + message = ctcp_message.message + action = True -def tagmsg(events, event): - from_self = _from_self(event["server"], event["direction"], - event.get("prefix", None)) - if from_self == None: - return + if not message == None: + kwargs["message"] = message + kwargs["message_split"] = message.split(" ") + kwargs["action"] = action - user = None - if "prefix" in event and not from_self: - user = event["server"].get_user(event["prefix"].nickname) + event_type = event["command"].lower() + if event["command"] == "PRIVMSG": + event_type = "message" - target = event["args"][0] - channel = None - if target[0] in event["server"].channel_types: - channel = event["server"].channels.get(target) + context = "channel" if is_channel else "private" + hook = events.on(direction).on(event_type).on(context) - direction = "send" if from_self else "received" - context = "channel" if channel else "private" - hook = events.on(direction).on("tagmsg").on(context) + if is_channel: + hook.call(channel=target_obj, **kwargs) + target_obj.buffer.add_message(user.nickname, message, action, + event["tags"], from_self) + else: + hook.call(**kwargs) - kwargs = {"server": event["server"], "tags": event["tags"]} + buffer_obj = target_obj + if not from_self: + buffer_obj = user - if channel: - hook.call(user=user, channel=channel, **kwargs) - elif event["server"].is_own_nickname(target): - hook.call(user=user, **kwargs) - elif from_self: - user = event["server"].get_user(target) - hook.call(user=user, **kwargs) + buffer_obj.buffer.add_message(user.nickname, message, action, + event["tags"], from_self) diff --git a/modules/line_handler/user.py b/modules/line_handler/user.py index f95ce4f4..c31a3b52 100644 --- a/modules/line_handler/user.py +++ b/modules/line_handler/user.py @@ -19,13 +19,13 @@ def handle_311(event): def quit(events, event): nickname = None if event["direction"] == utils.Direction.Recv: - nickname = event["prefix"].nickname + nickname = event["source"].nickname reason = event["args"].get(0) if event["direction"] == utils.Direction.Recv: - nickname = event["prefix"].nickname + nickname = event["source"].nickname if (not event["server"].is_own_nickname(nickname) and - not event["prefix"].hostmask == "*"): + not event["source"].hostmask == "*"): user = event["server"].get_user(nickname) event["server"].remove_user(user) events.on("received.quit").call(reason=reason, user=user, @@ -37,10 +37,10 @@ def quit(events, event): def nick(events, event): new_nickname = event["args"].get(0) - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) old_nickname = user.nickname - if not event["server"].is_own_nickname(event["prefix"].nickname): + if not event["server"].is_own_nickname(event["source"].nickname): events.on("received.nick").call(new_nickname=new_nickname, old_nickname=old_nickname, user=user, server=event["server"]) else: @@ -52,7 +52,7 @@ def nick(events, event): event["server"].change_user_nickname(old_nickname, new_nickname) def away(events, event): - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) message = event["args"].get(0) if message: user.away = True @@ -65,7 +65,7 @@ def away(events, event): events.on("received.away.off").call(user=user, server=event["server"]) def chghost(event): - nickname = event["prefix"].nickname + nickname = event["source"].nickname username = event["args"][0] hostname = event["args"][1] @@ -78,7 +78,7 @@ def chghost(event): target.hostname = hostname def setname(event): - nickname = event["prefix"].nickname + nickname = event["source"].nickname realname = event["args"][0] user = event["server"].get_user(nickname) @@ -88,7 +88,7 @@ def setname(event): event["server"].realname = realname def account(events, event): - user = event["server"].get_user(event["prefix"].nickname) + user = event["server"].get_user(event["source"].nickname) if not event["args"][0] == "*": user.identified_account = event["args"][0] diff --git a/modules/modules.py b/modules/modules.py index 12023cc4..557e82ba 100644 --- a/modules/modules.py +++ b/modules/modules.py @@ -26,8 +26,9 @@ class Module(ModuleManager.BaseModule): name = event["args_split"][0].lower() if name in self.bot.modules.modules: raise utils.EventError("Module '%s' is already loaded" % name) + definition = self.bot.modules.find_module(name) - self._catch(name, lambda: self.bot.modules.load_module(self.bot, name)) + self._catch(name, lambda: self.bot.modules.load_module(self.bot, definition)) event["stdout"].write("Loaded '%s'" % name) @utils.hook("received.command.unloadmodule", min_args=1) @@ -46,8 +47,8 @@ class Module(ModuleManager.BaseModule): def _reload(self, name): self.bot.modules.unload_module(name) - self.bot.modules.load_module(self.bot, name) - + definition = self.bot.modules.find_module(name) + self.bot.modules.load_module(self.bot, definition) @utils.hook("received.command.reloadmodule", min_args=1) def reload(self, event): """ @@ -66,25 +67,26 @@ class Module(ModuleManager.BaseModule): :help: Reload all modules :permission: reload-all-modules """ - reloaded = [] - failed = [] + success = [] + fail = [] for name in list(self.bot.modules.modules.keys()): try: - self._reload(name) + self.bot.modules.unload_module(name) except ModuleManager.ModuleWarning: continue except: - failed.append(name) - continue - reloaded.append(name) + fail.append(name) + load_success, load_fail = self.bot.load_modules(safe=True) + success.extend(load_success) + fail.extend(load_fail) - if reloaded and failed: + if success and fail: event["stdout"].write("Reloaded %d modules, %d failed" % ( - len(reloaded), len(failed))) - elif failed: + len(success), len(fail))) + elif fail: event["stdout"].write("Failed to reload all modules") else: - event["stdout"].write("Reloaded %d modules" % len(reloaded)) + event["stdout"].write("Reloaded %d modules" % len(success)) @utils.hook("received.command.enablemodule", min_args=1) def enable(self, event): diff --git a/modules/permissions/__init__.py b/modules/permissions/__init__.py index 3075af6a..3e2ce3f1 100644 --- a/modules/permissions/__init__.py +++ b/modules/permissions/__init__.py @@ -1,7 +1,7 @@ #--depends-on commands #--depends-on config -import base64, os +import base64, binascii, os import scrypt from src import ModuleManager, utils @@ -51,13 +51,11 @@ class Module(ModuleManager.BaseModule): (None, None)) return hash, salt - def _random_string(self, n): - return base64.b64encode(os.urandom(n)).decode("utf8") - def _make_salt(self): - return self._random_string(64) + return base64.b64encode(os.urandom(64)).decode("utf8") + def _random_password(self): - return self._random_string(32) + return binascii.hexlify(os.urandom(32)).decode("utf8") def _make_hash(self, password, salt=None): salt = salt or self._make_salt() diff --git a/modules/signals.py b/modules/signals.py index 4cdefc1d..522179c5 100644 --- a/modules/signals.py +++ b/modules/signals.py @@ -1,4 +1,4 @@ -import signal +import signal, sys from src import Config, ModuleManager, utils class Module(ModuleManager.BaseModule): @@ -19,11 +19,17 @@ class Module(ModuleManager.BaseModule): self.events.on("signal.interrupt").call(signum=signum) + written = False for server in self.bot.servers.values(): - server.socket.clear_send_buffer() - line = server.send_quit("Shutting down") - server.send_enabled = False - line.on_send(self._make_hook(server)) + if server.connected: + server.socket.clear_send_buffer() + line = server.send_quit("Shutting down") + server.send_enabled = False + line.on_send(self._make_hook(server)) + written = True + + if not written: + sys.exit() def _make_hook(self, server): return lambda: self.bot.disconnect(server) @@ -42,9 +48,6 @@ class Module(ModuleManager.BaseModule): self.bot.config.load() self.bot.log.info("Reloaded config file", []) - def _reload(self, name): - self.bot.modules.unload_module(name) - self.bot.modules.load_module(self.bot, name) def _reload_modules(self): self.bot.log.info("Reloading modules", []) diff --git a/modules/tweets.py b/modules/tweets.py deleted file mode 100644 index d100bb47..00000000 --- a/modules/tweets.py +++ /dev/null @@ -1,122 +0,0 @@ -#--depends-on commands -#--depends-on config - -#--require-config twitter-api-key -#--require-config twitter-api-secret -#--require-config twitter-access-token -#--require-config twitter-access-secret - -import datetime, html, re, time, traceback -import twitter -from src import ModuleManager, utils - -REGEX_TWITTERURL = re.compile( - "https?://(?:www\.)?twitter.com/[^/]+/status/(\d+)", re.I) - -@utils.export("channelset", {"setting": "auto-tweet", - "help": "Enable/disable automatically getting tweet info", - "validate": utils.bool_or_none, "example": "on"}) -class Module(ModuleManager.BaseModule): - _name = "Twitter" - - def make_timestamp(self, s): - seconds_since = time.time() - datetime.datetime.strptime(s, - "%a %b %d %H:%M:%S %z %Y").timestamp() - since, unit = utils.time_unit(seconds_since) - return "%s %s ago" % (since, unit) - - def _get_api(self): - api_key = self.bot.config["twitter-api-key"] - api_secret = self.bot.config["twitter-api-secret"] - access_token = self.bot.config["twitter-access-token"] - access_secret = self.bot.config["twitter-access-secret"] - return twitter.Twitter(auth=twitter.OAuth( - access_token, access_secret, api_key, api_secret)) - - def _from_id(self, tweet_id): - api = self._get_api() - try: - return api.statuses.show(id=tweet_id) - except: - traceback.print_exc() - - def _format_tweet(self, tweet): - linked_id = tweet["id"] - username = tweet["user"]["screen_name"] - - verified = "" - if tweet["user"]["verified"]: - verified = " %s" % utils.irc.color("✓", utils.consts.LIGHTBLUE) - - tweet_link = "https://twitter.com/%s/status/%s" % (username, - linked_id) - - short_url = self.exports.get_one("shortlink")(tweet_link) - short_url = " - %s" % short_url if short_url else "" - - if "retweeted_status" in tweet: - original_username = "@%s" % tweet["retweeted_status" - ]["user"]["screen_name"] - original_text = tweet["retweeted_status"]["text"] - retweet_timestamp = self.make_timestamp(tweet[ - "created_at"]) - original_timestamp = self.make_timestamp(tweet[ - "retweeted_status"]["created_at"]) - return "(@%s%s (%s) retweeted %s (%s)) %s%s" % ( - username, verified, retweet_timestamp, original_username, - original_timestamp, html.unescape(original_text), - short_url) - else: - return "(@%s%s, %s) %s%s" % (username, verified, - self.make_timestamp(tweet["created_at"]), - html.unescape(tweet["text"]), short_url) - - @utils.hook("received.command.tw", alias_of="tweet") - @utils.hook("received.command.tweet") - def tweet(self, event): - """ - :help: Get/find a tweet - :usage: [@username/URL/ID] - """ - - if event["args"]: - target = event["args"] - else: - target = event["target"].buffer.find(REGEX_TWITTERURL) - if target: - target = target.message - if target: - url_match = re.search(REGEX_TWITTERURL, target) - if url_match or target.isdigit(): - tweet_id = url_match.group(1) if url_match else target - tweet = self._from_id(tweet_id) - else: - if target.startswith("@"): - target = target[1:] - api = self._get_api() - try: - tweet = api.statuses.user_timeline( - screen_name=target, count=1)[0] - except: - traceback.print_exc() - tweet = None - if tweet: - tweet_str = self._format_tweet(tweet) - event["stdout"].write(tweet_str) - else: - event["stderr"].write("Invalid tweet identifiers provided") - else: - event["stderr"].write("No tweet provided to get information about") - - @utils.hook("command.regex", pattern=REGEX_TWITTERURL) - def regex(self, event): - """ - :command: tweet - """ - if event["target"].get_setting("auto-tweet", False): - event.eat() - tweet_id = event["match"].group(1) - tweet = self._from_id(tweet_id) - if tweet: - tweet_str = self._format_tweet(tweet) - event["stdout"].write(tweet_str) diff --git a/modules/tweets/__init__.py b/modules/tweets/__init__.py new file mode 100644 index 00000000..b0af2bd9 --- /dev/null +++ b/modules/tweets/__init__.py @@ -0,0 +1,165 @@ +#--depends-on commands +#--depends-on permissions +#--require-config twitter-api-key +#--require-config twitter-api-secret +#--require-config twitter-access-token +#--require-config twitter-access-secret + +import re +from src import ModuleManager, utils +from . import format +import tweepy + +_bot = None +_events = None +_exports = None + +REGEX_TWITTERURL = re.compile( + "https?://(?:www\.)?twitter.com/[^/]+/status/(\d+)", re.I) + +def _get_follows(): + return _bot.database.channel_settings.find_by_setting("twitter-follow") + +class BitBotStreamListener(tweepy.StreamListener): + def on_status(self, status): + data = json.loads(status) + username = data["user"]["screen_name"].lower() + + follows = [] + for server_id, channel_name, value in _get_follows(): + if value.lower() == username: + server = _bot.get_server_by_id(server_id) + if server and channel_name in server.channels: + hooks.append([server, server.channels.get(channel_name)]) + + tweet = format._tweet(_exports, data) + for server, channel in follows: + self.events.on("send.stdout").call(target=channel, + module_name="Tweets", server=server, message=tweet) + +@utils.export("channelset", {"setting": "auto-tweet", + "help": "Enable/disable automatically getting tweet info", + "validate": utils.bool_or_none, "example": "on"}) +class Module(ModuleManager.BaseModule): + _stream = None + def on_load(self): + global _bot + global _events + global _exports + _bot = self.bot + _events = self.events + _exports = self.exports + def unload(self): + self._dispose_stream() + + def _dispose_stream(self): + if not self._stream == None: + self._stream.disconnect() + + def _get_auth(self): + auth = tweepy.OAuthHandler(self.bot.config["twitter-api-key"], + self.bot.config["twitter-api-secret"]) + auth.set_access_token(self.bot.config["twitter-access-token"], + self.bot.config["twitter-access-secret"]) + return auth + def _get_api(self, auth): + return tweepy.API(auth) + + def _from_id(self, tweet_id): + return self._get_api(self._get_auth()).get_status(tweet_id) + def _from_username(self, username): + return self._get_api(self._get_auth()).user_timeline( + screen_name=username, count=1)[0] + + def _start_stream(self): + self._dispose_stream() + + usernames = set([]) + for server_id, channel_name, value in _get_follows(): + usernames.add(value) + if not usernames: + return False + + auth = self._get_auth() + self._stream = tweepy.Stream(auth=auth, listener=BitBotStreamListener) + + self._stream.filter(follow=list(usernames), is_async=True) + return True + + @utils.hook("received.command.tfollow", min_args=2, channel_only=True) + def tfollow(self, event): + """ + :help: Stream tweets from a given account to the current channel + :usage: add|remove @<username> + :permission: twitter-follow + """ + username = event["args_split"][0] + if username.startswith("@"): + username = username[1:] + + subcommand = event["args_split"][0].lower() + follows = event["target"].get_setting("twitter-follow", []) + action = None + + if subcommand == "add": + action = "followed" + if username in follows: + raise utils.EventError("Already following %s" % username) + follows.append(username) + elif subcommand == "remove": + action = "unfollowed" + if not username in follows: + raise utils.EventError("Not following %s" % username) + follows.remove(username) + else: + raise utils.EventError("Unknown subcommand") + + event["target"].set_setting("twitter-follow", follows) + self._start_stream() + event["stdout"].write("%s @%s" % (action.title(), username)) + + @utils.hook("received.command.tw", alias_of="tweet") + @utils.hook("received.command.tweet") + def tweet(self, event): + """ + :help: Get/find a tweet + :usage: [@username/URL/ID] + """ + + if event["args"]: + target = event["args"] + else: + target = event["target"].buffer.find(REGEX_TWITTERURL) + if target: + target = target.message + if target: + url_match = re.search(REGEX_TWITTERURL, target) + if url_match or target.isdigit(): + tweet_id = url_match.group(1) if url_match else target + tweet = self._from_id(tweet_id) + else: + if target.startswith("@"): + target = target[1:] + tweet = self._from_username(target) + + if tweet: + tweet_str = format._tweet(self.exports, tweet) + event["stdout"].write(tweet_str) + else: + event["stderr"].write("Invalid tweet identifiers provided") + else: + event["stderr"].write("No tweet provided to get information about") + + @utils.hook("command.regex", pattern=REGEX_TWITTERURL) + def regex(self, event): + """ + :command: tweet + """ + if event["target"].get_setting("auto-tweet", False): + event.eat() + tweet_id = event["match"].group(1) + tweet = self._from_id(tweet_id) + if tweet: + tweet_str = format._tweet(self.exports, tweet) + event["stdout"].write(tweet_str) + diff --git a/modules/tweets/format.py b/modules/tweets/format.py new file mode 100644 index 00000000..c41fde17 --- /dev/null +++ b/modules/tweets/format.py @@ -0,0 +1,35 @@ +import datetime, html, time +from src import utils + +def _timestamp(dt): + seconds_since = time.time()-dt.timestamp() + since, unit = utils.time_unit(seconds_since) + return "%s %s ago" % (since, unit) + +def _tweet(exports, tweet): + linked_id = tweet.id + username = tweet.user.screen_name + + verified = "" + if tweet.user.verified: + verified = " %s" % utils.irc.color("✓", utils.consts.LIGHTBLUE) + + tweet_link = "https://twitter.com/%s/status/%s" % (username, + linked_id) + + short_url = exports.get_one("shortlink")(tweet_link) + short_url = " - %s" % short_url if short_url else "" + created_at = _timestamp(tweet.created_at) + + # having to use hasattr here is nasty. + if hasattr(tweet, "retweeted_status"): + original_username = tweet.retweeted_status.user.screen_name + original_text = tweet.retweeted_status.text + original_timestamp = _timestamp(tweet.retweeted_status.created_at) + return "(@%s%s (%s) retweeted @%s (%s)) %s%s" % (username, verified, + created_at, original_username, original_timestamp, + html.unescape(original_text), short_url) + else: + return "(@%s%s, %s) %s%s" % (username, verified, created_at, + html.unescape(tweet.text), short_url) + |
