Skip to content

Commit 9f07dfc

Browse files
committed
Added command support.
1 parent 17c924e commit 9f07dfc

File tree

6 files changed

+171
-14
lines changed

6 files changed

+171
-14
lines changed

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ A (extensible) tunnel bot between chat protocols
99
- [x] Inter-channel communication
1010
- [x] Queue processing
1111
- [x] Multimedia
12-
- [ ] Inter-channel commands
12+
- [x] Inter-channel commands
13+
- [ ] Extra functions from slave channels
1314
- [ ] Telgeram Master Channel
1415
- [x] Basic Text processing
1516
- [ ] Controlling slaves by command
1617
- [x] Chat association
1718
- [x] Multimedia
1819
- [x] Generate chat head
19-
- [ ] Wechat Slave Channel
20+
- [ ] WeChat Slave Channel
2021
- [x] Basic Text/Link processing
2122
- [x] Multimedia
22-
- [ ] Add friends (Cards & Requests)
23+
- [x] Add friends (Cards & Requests)
24+
- [ ] Other actions
2325
- [ ] WhatsApp Slave Channel
2426
- [ ] Line Slave Channel
2527
- [ ] QQ Slave Channel

docs/message.md

+35-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,41 @@ Shared links.
182182
"url": URL in the message.
183183

184184
### Command
185-
> TODO: Command message documentation.
185+
**Type**: MsgType.Command
186+
**Additional Parameters**: attributes
187+
188+
This message is sent when a message from **slave** channel provides action options to the user. E.g.: Friend request,
189+
money transfer, etc.
190+
191+
`text` should include all necessary information for the user understand the situation and take action.
192+
193+
`attributes` is a dict with one item: `commands`, whose value is a list of commands with structure described below:
194+
195+
```python
196+
msg.attributes = {
197+
"commands": [
198+
{
199+
"name": "A human-readable name for the command",
200+
"callable": "name to the callable function in your channel object",
201+
"args": [
202+
"a list of positional parameters passed to your function"
203+
],
204+
"kwargs": {
205+
"desc": "a dict of keyword parameters passed to your function"
206+
}
207+
},
208+
{
209+
"name": "Greet @blueset on Telegram",
210+
"callable": "send_message_by_username",
211+
"args": [
212+
"blueset",
213+
"Hello!"
214+
],
215+
"kwargs": {}
216+
}
217+
]
218+
}
219+
```
186220

187221
### Unsupported message
188222
> TODO: Unsupported message documentation.

docs/slave-channel.md

+7
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,10 @@ Each `dict` item should have:
8383
### `get_group_members(self, group_uid)`
8484

8585
> TODO: Documentation for `get_group_members`
86+
87+
## Command message
88+
Slave channels can send messages that offer the user options to take action. This can be used in situations such as friend request, fund transfer and Telegram Inline Buttons.
89+
90+
Aside from sending a message with "Command" type (refer to the specification in "Message" documentation), the slave channel have to also provide a method for the command to be issued.
91+
92+
The method can take any argument of Python's standard data structure, and returns a string which is shown to the user as the result of the action.

docs/workflow.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,9 @@ To deliver user's message to a slave channel, you should first gather all the in
4141
## Slave Channel
4242
Slave channel has rather less things to do, get and enqueue incoming message, and send message to the platform. Also you may need to generate a list of possible recipients for the Master channel. That should be most of it.
4343

44-
> In the future, I may also come out with a protocol for command delivery. This includes some common ones like adding or accepting new contacts to slave channel, or even customizable ones. See when can I finish it. :P
44+
# Commands
45+
Once there's any message from a slave channel that allows the user to take action, the slave channel will enclose detail of actions (namely method names and arguments) into an `EFBMsg` object and send it to the master channel.
46+
47+
Master channel will use it's own method to ask the user to make a decision, and with the decision, the master channel will call the respective method of the slave channel with the argument given.
48+
49+
The method of slave channel returns a string as the result which is then reflected back to the user.

plugins/tg/__init__.py

+30-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class Flags:
2525
EXEC_LINK = 0x12
2626
# Start a chat
2727
START_CHOOSE_CHAT = 0x21
28-
pass
28+
# Command
29+
COMMAND_PENDING = 0x31
2930

3031

3132
class TelegramChannel(EFBChannel):
@@ -128,6 +129,8 @@ def callback_query_dispatcher(self, bot, update):
128129
self.link_chat_exec(bot, chat_id, msg_id, text)
129130
elif msg_status == Flags.START_CHOOSE_CHAT:
130131
self.make_chat_head(bot, chat_id, msg_id, text)
132+
elif msg_status == Flags.COMMAND_PENDING:
133+
self.command_exec(bot, chat_id, msg_id, text)
131134
else:
132135
bot.editMessageText(text="Session expired. Please try again.",
133136
chat_id=chat_id,
@@ -191,11 +194,11 @@ def process_msg(self, msg):
191194
else:
192195
tg_msg = self.bot.bot.sendMessage(tg_dest, text=msg_template % msg.text)
193196
elif msg.type in [MsgType.Image, MsgType.Sticker]:
194-
self.logger.info("recieved Image/Sticker \nPath: %s\nSize: %s\nMIME: %s", msg.path,
197+
self.logger.info("Received Image/Sticker \nPath: %s\nSize: %s\nMIME: %s", msg.path,
195198
os.stat(msg.path).st_size, msg.mime)
196199
if os.stat(msg.path).st_size == 0:
197200
os.remove(msg.path)
198-
return self.bot.bot.sendMessage(tg_dest, msg_template % ("Error: Empty %s recieved" % msg.type))
201+
return self.bot.bot.sendMessage(tg_dest, msg_template % ("Error: Empty %s received. (MS01)" % msg.type))
199202
if not msg.text:
200203
if msg.type == MsgType.Image:
201204
msg.text = "sent a picture."
@@ -209,7 +212,7 @@ def process_msg(self, msg):
209212
elif msg.type == MsgType.File:
210213
if os.stat(msg.path).st_size == 0:
211214
os.remove(msg.path)
212-
return self.bot.bot.sendMessage(tg_dest, msg_template % ("Error: Empty %s recieved" % msg.type))
215+
return self.bot.bot.sendMessage(tg_dest, msg_template % ("Error: Empty %s received. (MS02)" % msg.type))
213216
if not msg.text:
214217
file_name = os.path.basename(msg.path)
215218
msg.text = "sent a file."
@@ -220,7 +223,7 @@ def process_msg(self, msg):
220223
elif msg.type == MsgType.Audio:
221224
if os.stat(msg.path).st_size == 0:
222225
os.remove(msg.path)
223-
return self.bot.bot.sendMessage(tg_dest, msg_template % ("Error: Empty %s recieved" % msg.type))
226+
return self.bot.bot.sendMessage(tg_dest, msg_template % ("Error: Empty %s received. (MS03)" % msg.type))
224227
pydub.AudioSegment.from_file(msg.file).export("%s.ogg" % msg.path, format="ogg", codec="libopus")
225228
ogg_file = open("%s.ogg" % msg.path, 'rb')
226229
if not msg.text:
@@ -241,6 +244,13 @@ def process_msg(self, msg):
241244
msg.text = "sent a video."
242245
tg_msg = self.bot.bot.sendVideo(tg_dest, video=msg.file, caption=msg_template % msg.text)
243246
os.remove(msg.path)
247+
elif msg.type == MsgType.Command:
248+
buttons = []
249+
for i, ival in enumerate(msg.attributes['commands']):
250+
buttons.append([telegram.InlineKeyboardButton(ival['name'], callback_data=str(i))])
251+
tg_msg = self.bot.bot.send_message(tg_dest, msg_template % msg.text, reply_markup=telegram.InlineKeyboardMarkup(buttons))
252+
self.msg_status[tg_msg.message_id] = Flags.COMMAND_PENDING
253+
self.msg_storage[tg_msg.message_id] = {"channel": msg.channel_id, "commands": msg.attributes['commands']}
244254
else:
245255
tg_msg = self.bot.bot.sendMessage(tg_dest, msg_template % "Unsupported incoming message type. (UT01)")
246256
msg_log = {"master_msg_id": "%s.%s" % (tg_msg.chat.id, tg_msg.message_id),
@@ -361,7 +371,7 @@ def link_chat_gen_list(self, bot, chat_id, message_id=None, offset=0):
361371
for i in legend:
362372
msg_text += "%s\n" % i
363373

364-
msg = bot.editmMessageText(chat_id=chat_id, message_id=message_id, text=msg_text,
374+
msg = bot.editMessageText(chat_id=chat_id, message_id=message_id, text=msg_text,
365375
reply_markup=telegram.InlineKeyboardMarkup(chat_btn_list))
366376
self.msg_status[msg.message_id] = Flags.CONFIRM_LINK
367377

@@ -473,6 +483,20 @@ def make_chat_head(self, bot, tg_chat_id, tg_msg_id, callback_uid):
473483
db.add_msg_log(**msg_log)
474484
bot.editMessageText(text=txt, chat_id=tg_chat_id, message_id=tg_msg_id)
475485

486+
def command_exec(self, bot, chat_id, message_id, callback):
487+
if not callback.isdecimal():
488+
msg = "Invalid parameter: %s. (CE01)" % callback
489+
return bot.editMessageText(text=msg, chat_id=chat_id, message_id=message_id)
490+
elif not (0 <= int(msg) < len(self.msg_storage[message_id])):
491+
msg = "Index out of bound: %s. (CE02)" % callback
492+
return bot.editMessageText(text=msg, chat_id=chat_id, message_id=message_id)
493+
494+
callback = int(callback)
495+
channel_id = self.msg_storage[message_id]['channel']
496+
command = self.msg_storage[message_id]['comamnds'][callback]
497+
msg = getattr(self.slaves[channel_id], command['callable'])(*command['args'], **command['kwargs'])
498+
return bot.editMessageText(text=msg, chat_id=chat_id, message_id=message_id)
499+
476500
def msg(self, bot, update):
477501
target = None
478502
if not (update.message.chat.id == update.message.from_user.id): # from group

plugins/wechat.py

+88-3
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,22 @@ def wcVideo(msg):
148148
def wcVideoGroup(msg):
149149
self.videoMsg(msg, True)
150150

151+
@itchat.msg_register(['Card'])
152+
def wcCard(msg):
153+
self.cardMsg(msg)
154+
155+
@itchat.msg_register(['Card'], isGroupChat=True)
156+
def wcCardGroup(msg):
157+
self.cardMsg(msg, True)
158+
159+
@itchat.msg_register(['Friends'])
160+
def wcFriends(msg):
161+
self.friendMsg(msg)
162+
163+
@itchat.msg_register(['Friends'], isGroupChat=True)
164+
def wcFriendsGroup(msg):
165+
self.friendMsg(msg, True)
166+
151167
itchat.run()
152168
while True:
153169
if not itchat.client().status:
@@ -251,6 +267,63 @@ def videoMsg(self, msg, isGroupChat=False):
251267
mobj.file = open(mobj.path, "rb")
252268
return mobj
253269

270+
@incomeMsgMeta
271+
def cardMsg(self, msg, isGroupChat=False):
272+
mobj = EFBMsg(self)
273+
msg = """Name card: {NickName}
274+
From: {Province}, {City}
275+
QQ: {QQNum}
276+
ID: {Alias}
277+
Signature: {Signature}
278+
Gender: {Sex}"""
279+
msg = msg.format(**msg['Text'])
280+
mobj.text = msg
281+
mobj.type = MsgType.Command
282+
mobj.attributes = {
283+
"commands": [
284+
{
285+
"name": "Send friend request",
286+
"callable": "addFriend",
287+
"args": [],
288+
"kwargs": {
289+
"userName": msg['Text']['UserName'],
290+
"status": 2,
291+
"ticket": ""
292+
}
293+
}
294+
]
295+
}
296+
return mobj
297+
298+
@incomeMsgMeta
299+
def friendMsg(self, msg, isGroupChat=False):
300+
mobj = EFBMsg(self)
301+
msg = """Friend request: {NickName}
302+
Status: {Status}
303+
From: {Province}, {City}
304+
QQ: {QQNum}
305+
ID: {Alias}
306+
Signature: {Signature}
307+
Gender: {Sex}"""
308+
msg = msg.format(**{**msg['Text'], **msg['Text']['userInfo']})
309+
mobj.text = msg
310+
mobj.type = MsgType.Command
311+
mobj.attributes = {
312+
"commands": [
313+
{
314+
"name": "Send friend request",
315+
"callable": "addFriend",
316+
"args": [],
317+
"kwargs": {
318+
"userName": msg['Text']['userInfo']['UserName'],
319+
"status": 3,
320+
"ticket": msg['Ticket']
321+
}
322+
}
323+
]
324+
}
325+
return mobj
326+
254327
def save_file(self, msg, msg_type):
255328
path = os.path.join("storage", self.channel_id)
256329
if not os.path.exists(path):
@@ -259,11 +332,11 @@ def save_file(self, msg, msg_type):
259332
fullpath = os.path.join(path, filename)
260333
msg['Text'](fullpath)
261334
mime = magic.from_file(fullpath, mime=True).decode()
262-
ext = mimetypes.guess_extension(mime)
335+
ext = "jpg" if mime == "image/jpeg" else mimetypes.guess_extension(mime)
263336
os.rename(fullpath, "%s.%s" % (fullpath, ext))
264337
fullpath = "%s.%s" % (fullpath, ext)
265338
self.logger.info("File saved from WeChat\nFull path: %s\nMIME: %s", fullpath, mime)
266-
return (fullpath, mime)
339+
return fullpath, mime
267340

268341
def send_message(self, msg):
269342
"""Send a message to WeChat.
@@ -318,9 +391,21 @@ def send_message(self, msg):
318391
else:
319392
raise EFBMessageTypeNotSupported()
320393

394+
# Extra functions
395+
321396
@extra(name="Refresh Contacts and Groups list", desc="Refresh the list of contacts when unidentified contacts found.", emoji="🔁")
322397
def refresh_contacts(self):
323-
itchat.get_contract(true)
398+
itchat.get_contract(True)
399+
400+
# Command functions
401+
def add_friend(self, userName=None, status=2, ticket="", userInfo={}):
402+
if not userName:
403+
return "Username is empty. (UE01)"
404+
try:
405+
itchat.add_friend(userName, status, ticket, userInfo)
406+
return "Success."
407+
except:
408+
return "Error occurred during the process. (AF01)"
324409

325410
def get_chats(self, group=True, user=True):
326411
r = []

0 commit comments

Comments
 (0)