Skip to content

Commit

Permalink
added ability to define and delete mappings within the room with !rea…
Browse files Browse the repository at this point in the history
…cjibot command
  • Loading branch information
ajkessel committed Feb 3, 2024
1 parent 0323b07 commit f4b588c
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 20 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
This is a [Maubot](https://github.com/maubot/maubot) plugin for use in a [Matrix](https://matrix.org/) chat room. The plugin allows you to define arbitrary emoji reactions that will cause messages tagged with the specified emoji to be automatically cross-posted to a different room.
## Reacjibot
This is a [Maubot](https://github.com/maubot/maubot) plugin for use in a [Matrix](https://matrix.org/) chat room. The plugin allows you to define arbitrary emoji reactions that will cause messages tagged with the specified emoji to be automatically cross-posted to a different room. Once you have installed an initial configuration, you can define, undefine, and list mappings in the room with the `!reacji` command (or whatever you define as base_command in the configuration).

Example use cases could be tagging messages with :bulb: to post them to an #ideas channel for follow-up. Or if a message is off-topic, an emoji-reaction (say :train:) could cause the message to be reposted to the room with the correct topic for further discussion (say #public_transportation).

You'll need to create a config.yaml (based on [example-config.yaml](example-config.yaml)) to specify mappings. For example:
You'll need to create an initial config.yaml (based on [example-config.yaml](example-config.yaml)) to specify mappings. For example:
```
mapping:
💡: 'ideas'
Expand All @@ -19,6 +20,5 @@ If `insecure` is set to `true` (the default setting), the plugin will post from

## TODO

* add in-room commands to define mappings
* cull back unnecessary libraries
* allow multiple room posting with single emoji
6 changes: 6 additions & 0 deletions base-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ insecure: true
# allow images to be cross-posted (default true)
images: true

# Base command without the prefix (!).
# If a list is provided, the first is the main name and the rest are aliases.
base_command:
- reacji
- reacjibot

# specify emoji followed by channel name (either alias or Room ID)
# format is emoji: channel_name
# channel-name can be
Expand Down
6 changes: 6 additions & 0 deletions example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ insecure: true
# allow images to be cross-posted (default true)
images: true

# Base command without the prefix (!).
# If a list is provided, the first is the main name and the rest are aliases.
base_command:
- reacji
- reacjibot

# specify emoji followed by channel name (either alias or Room ID)
# format is emoji: channel_name
# channel-name can be
Expand Down
2 changes: 1 addition & 1 deletion maubot.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
maubot: 0.1.0
id: org.rosi-kessel.reacjibot
version: 2.0.5
version: 2.7.6
license: MIT
modules:
- reacjibot
Expand Down
123 changes: 107 additions & 16 deletions reacjibot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,29 @@ class ReacjiBot(Plugin):
repost: bool # preference for reposting tagged messages
images: bool # preference for crossposting images
template: str # template for cross-posting messages
base_command: str # command for talking to the bot
base_aliases: Tuple[str, ...] # aliases for bot base command

# UpdateReacji: re-process emoji->room mappings and look up rooms as required
async def UpdateReacji(self) -> None:
for key in self.config["mapping"]:
room=self.config["mapping"][key]
if room.find(":") == -1:
room = room + ":" + self.config["domain"]
if room[0] == "#":
room = room[1:]
if room[0] != "!":
try:
room = (await self.client.resolve_room_alias('#' + room)).room_id
except:
self.debug and self.log.debug(f"no valid room mapping found for {room}")
room = ""
self.debug and self.log.debug(f"mapping {key} to {room}")
room = await self.MapRoom(self.config["mapping"][key])
self.reacji[key] = room
self.debug and self.log.debug(f"mapping {key} to {room}")

# MapRoom: find room ID based on alias or fully qualified room name
async def MapRoom(self, room_candidate: str) -> str:
if room_candidate.find(":") == -1:
room_candidate = room_candidate + ":" + self.config["domain"]
if room_candidate[0] == "#":
room_candidate = room_candidate[1:]
if room_candidate[0] != "!":
try:
room_candidate = (await self.client.resolve_room_alias('#' + room_candidate)).room_id
except:
self.debug and self.log.debug(f"no valid room mapping found for {room_candidate}")
room_candidate = ""
return room_candidate

# IsEncrypted: check if room_id is an encrypted room, used if insecure is set to false
async def IsEncrypted(self, room_id) -> None:
Expand Down Expand Up @@ -80,10 +86,14 @@ async def start(self) -> None:
self.restrict = False
self.repost = False
self.images = True
self.base_command = 'reacji'
self.template = '[%on](%ol): %m \n\n ([%e](%bl) by [%bu](%bi))'
try:
self.debug = self.config["debug"]
self.debug and self.log.debug(f"verbose debugging enabled in config.yaml")
bc = self.config["base_command"]
self.base_command = bc[0] if isinstance(bc, list) else bc
self.base_aliases = tuple(bc) if isinstance(bc, list) else (bc,)
if self.config['template']:
self.template = self.config["template"]
self.debug and self.log.debug(f"got template {self.template}")
Expand All @@ -102,8 +112,7 @@ async def on_external_config_update(self) -> None:
await self.start()

# generic_react: called when a reaction to a message event occurs; main guts of the plugin
# TODO - find a better regexp that only matches emojis
@command.passive(regex=re.compile(r"[^A-Za-z0-9]"), field=lambda evt: evt.content.relates_to.key, event_type=EventType.REACTION, msgtypes=None)
@command.passive(regex=re.compile('[\U00010000-\U0010ffff]+', flags=re.UNICODE), field=lambda evt: evt.content.relates_to.key, event_type=EventType.REACTION, msgtypes=None)
async def generic_react(self, evt: ReactionEvent, key: Tuple[str]) -> None:
if self.restrict and evt.sender not in self.allowed:
self.debug and self.log.debug(f"user {evt.sender} not allowed to cross-post")
Expand All @@ -129,7 +138,7 @@ async def generic_react(self, evt: ReactionEvent, key: Tuple[str]) -> None:
displayname = await self.client.get_displayname(source_evt.sender)
# name of the room original message was posted in
roomnamestate = await self.client.get_state_event(source_evt.room_id, 'm.room.name')
roomname = str(roomnamestate['name'])
rn = str(roomnamestate['name'])
# userlink: a hyperlink to the original poster's user ID
userlink = str(MatrixURI.build(source_evt.sender))
# body: the contents of the message to be cross-posted
Expand All @@ -150,7 +159,7 @@ async def generic_react(self, evt: ReactionEvent, key: Tuple[str]) -> None:
message = message.replace('%bl',xmessage)
message = message.replace('%bu',xdisplayname)
message = message.replace('%bi',xuserlink)
message = message.replace('%rn',roomname)
message = message.replace('%rn',rn)
try:
self.debug and self.log.debug(f"posting {message} to {target_id}")
if source_evt.content.msgtype == MessageType.IMAGE:
Expand All @@ -170,3 +179,85 @@ async def generic_react(self, evt: ReactionEvent, key: Tuple[str]) -> None:
@classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]:
return Config

@command.new(name=lambda self: self.base_command,
aliases=lambda self, alias: alias in self.base_aliases,
help="Interact with reacjibot", require_subcommand=False)
async def reacji(self, evt: MessageEvent) -> None:
await evt.reply(f"reacjibot at your service\n\n!{self.base_command} help for help\n")

@reacji.subcommand("help", help="Usage instructions")
async def help(self, evt: MessageEvent) -> None:
await evt.reply(f"Maubot [Reacjibot](https://github.com/ajkessel/reacjibot) plugin.\n\n"
f"* !{self.base_command} map emoji room_name - map emoji to room\n"
f"* !{self.base_command} delete emoji - remove emoji mapping\n"
f"* !{self.base_command} list [optional emoji name] - list reacji mappings\n"
f"* !{self.base_command} help - this message\n")

@reacji.subcommand("list", help="List reacji mappings")
@command.argument("emojus", pass_raw=False, required=False)
async def list(self, evt: MessageEvent, emojus: str) -> None:
mappings=""
if emojus:
try:
xroom = str(MatrixURI.build(self.reacji[emojus]))
mappings = mappings + f"* {emojus} to {xroom}\n"
except:
mappings = f"* {emojus} is not mapped\n"
else:
for key in self.reacji:
xroom = str(MatrixURI.build(self.reacji[key]))
mappings = mappings + f"* {key} to {xroom}\n"
await evt.reply(mappings)
return

@reacji.subcommand("map", help="Define a emoji-room mapping")
@command.argument("mapping", pass_raw=True, required=True)
async def map(self, evt: MessageEvent, mapping: str) -> None:
if self.restrict and evt.sender not in self.allowed:
await evt.reply(f"Sorry, you are not allowed to configure reacjibot. Please ask your site administrator for permission.\n")
self.debug and self.log.debug(f"user {evt.sender} not allowed to configure")
return
try:
x = mapping.split(" ")
re_emoji = re.compile('[\U00010000-\U0010ffff]+', flags=re.UNICODE)
re_html = re.compile(r'<.*?>')
emoji = re_emoji.findall(x[0])
room_candidate = re_html.sub('',x[1])
room = await self.MapRoom(room_candidate)
xroom = str(MatrixURI.build(room))
finally:
if not emoji or not room:
await evt.reply(f"error, invalid mapping {mapping}")
return
for emojus in emoji:
await evt.reply(f"mapping {emojus} to {xroom}")
self.reacji[emojus] = room
self.config["mapping"][emojus] = room
self.config.save()
return

@reacji.subcommand("delete", help="Delete a emoji-room mapping", aliases=("del","erase","unmap"))
@command.argument("mapping", pass_raw=True, required=True)
async def delete(self, evt: MessageEvent, mapping: str) -> None:
if self.restrict and evt.sender not in self.allowed:
await evt.reply(f"Sorry, you are not allowed to configure reacjibot. Please ask your site administrator for permission.\n")
self.debug and self.log.debug(f"user {evt.sender} not allowed to configure")
return
try:
x = mapping.split(" ")
re_emoji = re.compile('[\U00010000-\U0010ffff]+', flags=re.UNICODE)
emoji = re_emoji.findall(x[0])
finally:
if not emoji:
await evt.reply(f"error, invalid delete command {mapping}")
return
for emojus in emoji:
try:
del self.reacji[emojus]
del self.config["mapping"][emojus]
await evt.reply(f"deleting mapping for {emojus}")
except:
await evt.reply(f"{emojus} is not mapped")
self.config.save()
return

0 comments on commit f4b588c

Please sign in to comment.