process_message(
routing_key=routing_key,
- body=self.get_amqp_message_body(msg, nick_offset, recipient)
+ body=self.get_amqp_message_body(msg, nick_offset, recipient, self.get_muc_rank(msg))
)
+ def get_muc_rank(self, msg) -> dict:
+ """MUC role/affiliation of the sender, from the xep_0045 presence roster (no round-trip).
+
+ Workers never see XMPP, so this is the only way for a plugin to know whether a
+ sender is e.g. a moderator. Empty for non-groupchat messages or unknown occupants.
+ """
+ if msg["type"] != "groupchat":
+ return {}
+ muc = self.plugin["xep_0045"]
+ try:
+ return {
+ "role": muc.get_jid_property(msg["mucroom"], msg["mucnick"], "role"),
+ "affiliation": muc.get_jid_property(msg["mucroom"], msg["mucnick"], "affiliation"),
+ }
+ except Exception as e:
+ logger.warning("could not look up MUC rank of %s: %s", msg["from"], e)
+ return {}
+
@staticmethod
- def get_amqp_message_body(msg, nick_offset, recipient) -> str:
+ def get_amqp_message_body(msg, nick_offset, recipient, rank=None) -> str:
return json.dumps({
"from": msg["from"].jid,
"to": recipient,
- "body": msg["body"][nick_offset:].strip()
+ "body": msg["body"][nick_offset:].strip(),
+ **(rank or {}),
})
def echo(self, body: str, recipient: str = None):
bot_nickname = string
bot_owner = string
-# nicks allowed to activate/deactivate plugins at runtime
+# nicks allowed to activate/deactivate plugins at runtime (room moderators always may)
admins = string_list(default=list())
bot_owner_email = string
detectlanguage_api_key = string
self.build_reverse_lookup()
return self.reverse_lookup.get(name)
- def set_plugin_state(self, msg, plugin_name: str, enabled: bool) -> Action:
- nick = get_nick_from_message(msg)
+ @staticmethod
+ def is_admin(msg) -> bool:
+ """Room moderators (MUC role, attached by the chat daemon) or nicks listed in `admins`."""
+ if msg.get("role") == "moderator":
+ return True
admins = conf_get("admins") or []
if isinstance(admins, str):
# "admins = TRex" without the trailing comma fails string_list validation, but
# Config() doesn't enforce it, so guard against "in <str>" turning into a substring check
admins = [admins]
- if nick not in admins:
+ return get_nick_from_message(msg) in admins
+
+ def set_plugin_state(self, msg, plugin_name: str, enabled: bool) -> Action:
+ nick = get_nick_from_message(msg)
+ if not self.is_admin(msg):
return Action(msg="{}: you are not allowed to do that".format(nick))
resolved = self.resolve_plugin_name(plugin_name)
msg = {"mucnick": "someRealUser", "type": "chat"}
assert Bot.muc_message(fake, msg) is False
fake.message.assert_not_called()
+
+
+def _muc_msg(nick="Alice", room="room@conf", type_="groupchat"):
+ from_ = Mock()
+ from_.jid = f"{room}/{nick}"
+ return {"type": type_, "mucroom": room, "mucnick": nick, "from": from_, "body": "urlbug: plugin deactivate dice"}
+
+
+def test_muc_rank_comes_from_xep_0045_roster():
+ fake = _fake_bot()
+ fake.plugin = {"xep_0045": Mock()}
+ fake.plugin["xep_0045"].get_jid_property.side_effect = \
+ lambda room, nick, prop: {"role": "moderator", "affiliation": "admin"}[prop]
+ assert Bot.get_muc_rank(fake, _muc_msg()) == {"role": "moderator", "affiliation": "admin"}
+ fake.plugin["xep_0045"].get_jid_property.assert_any_call("room@conf", "Alice", "role")
+
+
+def test_muc_rank_empty_for_private_chat():
+ fake = _fake_bot()
+ fake.plugin = {"xep_0045": Mock()}
+ assert Bot.get_muc_rank(fake, _muc_msg(type_="chat")) == {}
+ fake.plugin["xep_0045"].get_jid_property.assert_not_called()
+
+
+def test_muc_rank_lookup_failure_is_not_fatal():
+ fake = _fake_bot()
+ fake.plugin = {"xep_0045": Mock()}
+ fake.plugin["xep_0045"].get_jid_property.side_effect = KeyError("unknown occupant")
+ assert Bot.get_muc_rank(fake, _muc_msg()) == {}
+
+
+def test_message_body_carries_rank():
+ import json
+ body = json.loads(Bot.get_amqp_message_body(_muc_msg(), len("urlbug: "), "room@conf",
+ {"role": "moderator", "affiliation": "owner"}))
+ assert body == {"from": "room@conf/Alice", "to": "room@conf", "body": "plugin deactivate dice",
+ "role": "moderator", "affiliation": "owner"}
+
+
+def test_message_body_without_rank_is_unchanged():
+ import json
+ body = json.loads(Bot.get_amqp_message_body(_muc_msg(), len("urlbug: "), "room@conf"))
+ assert body == {"from": "room@conf/Alice", "to": "room@conf", "body": "plugin deactivate dice"}
Action(msg="Rex: you are not allowed to do that")
assert _run(helper, ["nick", "plugin", "deactivate", "dice"], sender="room@conf/TRex") == \
Action(msg="dice deactivated")
+
+
+def test_moderator_role_is_admin_without_being_listed(helper, state):
+ helper.used_routing_key = ["nick", "plugin", "deactivate", "dice"]
+ msg = {"from": USER, "to": "room@conf", "body": "", "role": "moderator", "affiliation": "admin"}
+ assert helper.parse_body(msg) == Action(msg="dice deactivated")
+ assert state == {"dice": False}
+
+
+def test_participant_role_is_not_admin(helper, state):
+ helper.used_routing_key = ["nick", "plugin", "deactivate", "dice"]
+ msg = {"from": USER, "to": "room@conf", "body": "", "role": "participant", "affiliation": "member"}
+ assert helper.parse_body(msg) == Action(msg="Someone: you are not allowed to do that")
+ assert state == {}