Skip to content

Commit 1896d22

Browse files
committed
Initial implemenation of room associations and audio processing
1 parent ad037e0 commit 1896d22

8 files changed

+771
-10
lines changed

bot_commands.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
class Command(object):
3535
"""Use this class for your bot commands."""
3636

37-
def __init__(self, client, store, config, command_dict, command, room, event):
37+
def __init__(self, client, store, config, command_dict, command, room_dict, room, event):
3838
"""Set up bot commands.
3939
4040
Arguments:
@@ -44,6 +44,7 @@ def __init__(self, client, store, config, command_dict, command, room, event):
4444
config (Config): Bot configuration parameters
4545
command_dict (CommandDict): Command dictionary
4646
command (str): The command and arguments
47+
room_dict (CommandDict): Room dictionary
4748
room (nio.rooms.MatrixRoom): The room the command was sent in
4849
event (nio.events.room_events.RoomMessageText): The event
4950
describing the command
@@ -54,6 +55,7 @@ def __init__(self, client, store, config, command_dict, command, room, event):
5455
self.config = config
5556
self.command_dict = command_dict
5657
self.command = command
58+
self.room_dict = room_dict
5759
self.room = room
5860
self.event = event
5961
# self.args: list : list of arguments
@@ -81,6 +83,18 @@ async def process(self): # noqa
8183
):
8284
await self._show_help()
8385

86+
# command from room dict
87+
elif self.room_dict.match(self.room.display_name):
88+
matched_cmd = self.room_dict.get_last_matched_room()
89+
await self._os_cmd(
90+
cmd=self.room_dict.get_cmd(matched_cmd),
91+
args=self.room_dict.get_opt_args(matched_cmd),
92+
markdown_convert=self.room_dict.get_opt_markdown_convert(matched_cmd),
93+
formatted=self.room_dict.get_opt_formatted(matched_cmd),
94+
code=self.room_dict.get_opt_code(matched_cmd),
95+
split=self.room_dict.get_opt_split(matched_cmd),
96+
)
97+
8498
# command from command dict
8599
elif self.command_dict.match(self.commandlower):
86100
matched_cmd = self.command_dict.get_last_matched_command()
@@ -139,9 +153,10 @@ async def _unknown_command(self):
139153
self.client,
140154
self.room.room_id,
141155
(
142-
f"Unknown command `{self.command}`. "
143-
"Try the `help` command for more information."
156+
f"{self.command}\n"
157+
"Try the *help* command for more information."
144158
),
159+
split="\n",
145160
)
146161

147162
async def _os_cmd(
@@ -175,7 +190,10 @@ async def _os_cmd(
175190
"""
176191
try:
177192
# create a combined argv list, e.g. ['date', '--utc']
178-
argv_list = [cmd] + args
193+
argv_list = [cmd]
194+
if args is not None:
195+
argv_list += args
196+
179197
logger.debug(
180198
f'OS command "{argv_list[0]}" with ' f'args: "{argv_list[1:]}"'
181199
)
@@ -192,11 +210,13 @@ async def _os_cmd(
192210

193211
run = subprocess.Popen(
194212
argv_list, # list of argv
213+
stdin=subprocess.PIPE,
195214
stdout=subprocess.PIPE,
196215
stderr=subprocess.PIPE,
197216
universal_newlines=True,
198217
env=new_env,
199218
)
219+
run.stdin.write( self.command )
200220
output, std_err = run.communicate()
201221
output = output.strip()
202222
std_err = std_err.strip()

callbacks.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,17 @@
2828
KeyVerificationMac,
2929
ToDeviceError,
3030
LocalProtocolError,
31+
DownloadError,
32+
DownloadResponse,
3133
)
34+
from nio.crypto import decrypt_attachment
35+
3236
import logging
3337
import traceback
38+
from base64 import b64encode
39+
from urllib.parse import urlparse
3440
from command_dict import CommandDict
41+
from room_dict import RoomDict
3542

3643
logger = logging.getLogger(__name__)
3744

@@ -55,6 +62,7 @@ def __init__(self, client, store, config):
5562
self.config = config
5663
self.command_dict = CommandDict(config.command_dict_filepath)
5764
self.command_prefix = config.command_prefix
65+
self.room_dict = RoomDict(config.room_dict_filepath)
5866

5967
async def message(self, room, event):
6068
"""Handle an incoming message event.
@@ -98,7 +106,46 @@ async def message(self, room, event):
98106
msg = msg[len(self.command_prefix):]
99107

100108
command = Command(self.client, self.store,
101-
self.config, self.command_dict, msg, room, event)
109+
self.config, self.command_dict, msg, self.room_dict, room, event)
110+
await command.process()
111+
112+
async def audio(self, room, event):
113+
"""Handle an incoming audio event.
114+
115+
Arguments:
116+
---------
117+
room (nio.rooms.MatrixRoom): The room the event came from
118+
event (nio.events.room_events.RoomMessageAudio): The event
119+
defining the (audio) message
120+
121+
"""
122+
# Ignore messages from ourselves
123+
if event.sender == self.client.user:
124+
return
125+
126+
# Ignore non-ogg audio
127+
if event.body[-4:] != '.ogg':
128+
logger.debug(
129+
f"Bot received (apparently) non-ogg data for room {room.display_name} | "
130+
f"{room.user_name(event.sender)}: {event.body}"
131+
)
132+
return
133+
134+
# download the audio data
135+
logger.debug(
136+
f"Bot downloading audio for room {room.display_name} | "
137+
f"{room.user_name(event.sender)}: {event.url}"
138+
)
139+
mxc = urlparse(event.url)
140+
response = await self.client.download(server_name=mxc.netloc, media_id=mxc.path.strip("/"), filename=None, allow_remote=True)
141+
if isinstance(response, DownloadError):
142+
logger.error(f"Bot download of media resulted in error")
143+
return
144+
data = decrypt_attachment(response.body, event.key["k"], event.hashes["sha256"], event.iv)
145+
data = f"data:audio/ogg;base64,{b64encode(data).decode('utf-8')}"
146+
147+
command = Command(self.client, self.store,
148+
self.config, self.command_dict, data, self.room_dict, room, event)
102149
await command.process()
103150

104151
async def invite(self, room, event):

command_dict.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,7 @@ def match(self, string):
176176
cmd = command
177177
break
178178

179-
if cmd and len(cmd) > 0:
180-
self._last_matched_command = cmd
181-
else:
182-
self._last_matched_command = None
179+
self._last_matched_command = cmd
183180

184181
return matched
185182

config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def __init__(self, filepath):
7575
["storage", "store_filepath"], required=True)
7676
self.command_dict_filepath = self._get_cfg(
7777
["storage", "command_dict_filepath"], default=None)
78+
self.room_dict_filepath = self._get_cfg(
79+
["storage", "room_dict_filepath"], default=None)
7880

7981
# Create the store folder if it doesn't exist
8082
if not os.path.isdir(self.store_filepath):
@@ -112,6 +114,10 @@ def __init__(self, filepath):
112114
["matrix", "trust_own_devices"], default=False, required=False)
113115
self.change_device_name = self._get_cfg(
114116
["matrix", "change_device_name"], default=False, required=False)
117+
self.process_audio = self._get_cfg(
118+
["matrix", "process_audio"], default=False, required=False)
119+
self.accept_invitations = self._get_cfg(
120+
["matrix", "accept_invitations"], default=True, required=False)
115121

116122
def _get_cfg(
117123
self,

config.yaml.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ matrix:
4848
# If true, device_name of bot will be changed to value given in device_name.
4949
change_device_name: false
5050
# encrytion is enabled by default
51+
accept_invitations: true
52+
# Should the bot accept invitations? If disabled then the bot must be
53+
# added to the desired rooms "manually". This configuration is mostly
54+
# useful in combination with the room-specific processing "feature"
55+
# (refer to storage.room_dict_path)
56+
process_audio: false
57+
# Should the bot also process audio messages (need to be downloaded
58+
# before processing)? If true the audio content will be passed (just
59+
# as with text content) to the handling command program, but base64
60+
# encoded inside a "data:" url
5161

5262
storage:
5363
# The path to the database
@@ -57,6 +67,8 @@ storage:
5767
store_filepath: "./store"
5868
# The path to the command dictionary configuration file
5969
command_dict_filepath: "./commands.yaml"
70+
# The path to the room dictionary configuration file
71+
room_dict_filepath: "./room.yaml"
6072

6173
# Logging setup
6274
logging:

main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
AsyncClient,
2828
AsyncClientConfig,
2929
RoomMessageText,
30+
RoomMessageAudio,
31+
RoomEncryptedAudio,
3032
InviteMemberEvent,
3133
LoginError,
3234
LocalProtocolError,
@@ -78,7 +80,10 @@ async def main(): # noqa
7880
# Set up event callbacks
7981
callbacks = Callbacks(client, store, config)
8082
client.add_event_callback(callbacks.message, (RoomMessageText,))
81-
client.add_event_callback(callbacks.invite, (InviteMemberEvent,))
83+
if config.accept_invitations:
84+
client.add_event_callback(callbacks.invite, (InviteMemberEvent,))
85+
if config.process_audio:
86+
client.add_event_callback(callbacks.audio, (RoomEncryptedAudio,))
8287
client.add_to_device_callback(
8388
callbacks.to_device_cb, (KeyVerificationEvent,))
8489

0 commit comments

Comments
 (0)