Skip to content

Commit ae832cc

Browse files
author
Pietro Albini
committed
Add support for moderating group chats
The Bot API 2.0 update added support for moderating group chats from your bot, and this commit adds support for that in botogram. There are two new methods introduced in this commit (along with their docs): - botogram.Chat.ban() - botogram.Chat.unban() Fixes: GH-56
1 parent cd54c91 commit ae832cc

File tree

5 files changed

+266
-0
lines changed

5 files changed

+266
-0
lines changed

botogram/objects/chats.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,38 @@ def leave(self):
216216
raise exc from None
217217
raise
218218

219+
@mixins._require_api
220+
def ban(self, user):
221+
"""Ban an user from the group"""
222+
# Check if the chat is a group
223+
if self.type not in ("group", "supergroup"):
224+
raise RuntimeError("This chat is not a group or a supergroup!")
225+
226+
# Accept also an instance of `User`
227+
if isinstance(user, User):
228+
user = user.id
229+
230+
self._api.call("kickChatMember", {
231+
"chat_id": self.id,
232+
"user_id": user,
233+
})
234+
235+
@mixins._require_api
236+
def unban(self, user):
237+
"""Unban an user from the supergroup"""
238+
# Check if the chat is a supergroup
239+
if self.type != "supergroup":
240+
raise RuntimeError("This chat is nota group or a supergroup!")
241+
242+
# Accept also an instance of `User`
243+
if isinstance(user, User):
244+
user = user.id
245+
246+
self._api.call("unbanChatMember", {
247+
"chat_id": self.id,
248+
"user_id": user,
249+
})
250+
219251

220252
class ChatMember(BaseObject):
221253
"""Telegram API representation of a chat member

docs/api/telegram.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,67 @@ about its business.
531531
532532
.. versionadded:: 0.3
533533

534+
.. py:method:: ban(user)
535+
536+
Ban the provided user from this group chat. You can either provide the
537+
user ID or an instance of :py:class:`~botogram.User`. This method is the
538+
cornerstone of :ref:`moderating group chats <manage-chats>`, since it
539+
allows your bot to punish misbehaving users.
540+
541+
While on normal group chats a banned user can rejoin the chat if it's
542+
added by one of its members or he uses a join link, on supergroups you
543+
need to explicitly :py:meth:`~botogram.Chat.unban` he to let him rejoin.
544+
545+
Remember your bot must be an administrator of the chat in order to this
546+
method to work properly.
547+
548+
.. code-block:: python
549+
550+
# This command bans the user who sent the message you replied to
551+
552+
@bot.command("ban")
553+
def ban_user(chat, message, args):
554+
"""Ban that user"""
555+
# Some needed filtering and error handling
556+
if message.reply_to_message is None:
557+
message.reply("You must reply to a message the user wrote!")
558+
if message.sender not in chat.admins:
559+
message.reply("You must be an admin of the group!")
560+
if message.reply_to_message.sender in chat.admins:
561+
message.reply("You can't ban another admin!")
562+
563+
chat.ban(message.reply_to_message.sender)
564+
565+
:param int user: The user you want to ban (user ID or
566+
:py:class:`~botogram.User`)
567+
568+
.. versionadded:: 0.3
569+
570+
.. py:method:: unban(user)
571+
572+
Unban the user from this group chat. This does nothing on normal group
573+
chats, but it removes the user from the group's blacklist if the chat is
574+
a supergroup. This method can be handy if you want to remove the ban you
575+
given to an user.
576+
577+
Remember your bot must be an administrator of the chat in order to this
578+
method to work properly.
579+
580+
.. code-block:: python
581+
582+
@bot.timer(60)
583+
def unban_all(shared, bot):
584+
# This unbans all the users in the shared memory
585+
for chat_id, user_id in shared["banned_users"].items():
586+
bot.chat(chat_id).unban(user_id)
587+
588+
shared["banned_users"] = {}
589+
590+
:param int user: The user you want to unban (user ID or
591+
:py:class:`~botogram.User`)
592+
593+
.. versionadded:: 0.3
594+
534595
.. py:method:: send(message, [preview=True, reply_to=None, syntax=None, extra=None, notify=True])
535596
536597
Send the textual *message* to the chat. You may optionally stop clients

docs/changelog/0.3.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ New features
4848
* New decorator :py:meth:`botogram.Bot.message_edited`
4949
* New method :py:meth:`botogram.Component.add_message_edited_hook`
5050

51+
* Added support for moderating groups:
52+
53+
* New method :py:meth:`botogram.Chat.ban`
54+
* New method :py:meth:`botogram.Chat.unban`
55+
5156
* Added support for sending contacts:
5257

5358
* New method :py:meth:`botogram.User.send_contact`

docs/groups-management.rst

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
.. Copyright (c) 2016 Pietro Albini <[email protected]>
2+
Released under the MIT license
3+
4+
.. _groups-management:
5+
6+
======================
7+
Moderating group chats
8+
======================
9+
10+
Group chats are an awesome way to communicate with your circle of friends, but
11+
you can also use them to discuss with other users in public groups.
12+
Unfortunately, there are people who just want to spam their own groups or post
13+
inappropriate content and, without some moderators always online, a public
14+
group will get messy sooner or later.
15+
16+
Telegram bots are able to moderate groups easily, so you can programmatically
17+
ban and unban users of the groups your bot is an administrator of.
18+
19+
.. versionadded:: 0.3
20+
21+
.. _groups-management-preparation:
22+
23+
Allowing your bot to moderate groups
24+
====================================
25+
26+
Bots can't moderate groups by default, but they need to be authorized to do so.
27+
How to authorize them is specific to the client you use, but take the following
28+
advices:
29+
30+
* If you're trying to add your bot as administrator to a **group**, you need to
31+
disable the "Everyone is an admin" setting of it, and then explicitly mark
32+
your bot as administrator.
33+
34+
* If you're trying to add your bot as administrator to a **supergroup**, just
35+
add it to the administrators list.
36+
37+
Keep in mind your bot might lose its status of administrator during the
38+
conversion from a group to a supergroup, so if you converted a group check if
39+
your bot is still an administrator of it.
40+
41+
.. _groups-management-ban:
42+
43+
Banning nasty users
44+
===================
45+
46+
Nobody wants users joining a public groups just to spam their own group but, if
47+
there isn't a moderator always online, a spammer can join in the middle of the
48+
night, and multiple users might see their message before a moderator gets
49+
online to ban him.
50+
51+
As an example, with the aid of :py:class:`~botogram.ParsedText` you can detect
52+
all the posted URLs, and ban all the user who sends a ``telegram.me`` link,
53+
with :py:meth:`botogram.Chat.ban`. Unfortunately bots can't delete messages
54+
yet, but they can mention all the administrators of the group, so they can
55+
erase that message later:
56+
57+
.. code-block:: python
58+
:emphasize-lines: 15,16
59+
60+
@bot.process_message
61+
def ban_telegram_links(chat, message):
62+
# Don't do anything if there is no text message
63+
if message.text is None:
64+
return
65+
66+
# Check all the links in the message
67+
for link in message.parsed_text.filter("link"):
68+
# Match both HTTP and HTTPS links
69+
if "://telegram.me" in link.url:
70+
break
71+
else:
72+
return
73+
74+
# Ban the sender of the message
75+
chat.ban(message.sender)
76+
77+
# Tell all the admins of the chat to delete the message
78+
admins = " ".join("@"+a.username for a in chat.admins if a.username)
79+
if admins:
80+
message.reply("%s! Please delete this message!" % admins)
81+
return
82+
83+
.. _groups-management-unban:
84+
85+
Unbanning users from supergroups
86+
================================
87+
88+
If an user is banned from a group chat, he can rejoin the group if he uses a
89+
join link or it's added by another member. Instead, when an user is banned from
90+
a supergroup he's put in a blacklist, and he needs to be explicitly removed
91+
from it if he want to join the supergroup again.
92+
93+
The following example is a working tempban code, which stores when the user was
94+
banned in :ref:`the shared memory <shared-memory>`, with a :ref:`timer
95+
<tasks-repeated>` to unban the user later. The actual unban is executed via the
96+
:py:meth:`botogram.Chat.unban` method:
97+
98+
.. code-block:: python
99+
:emphasize-lines: 60
100+
101+
import time
102+
103+
# This means `/tempban 1` bans the user for 60 seconds
104+
BAN_DURATION_MULTIPLIER = 60
105+
106+
@bot.prepare_memory
107+
def prepare_memory(shared):
108+
shared["bans"] = {}
109+
110+
@bot.command("tempban")
111+
def tempban_command(shared, chat, message, args):
112+
"""Tempban an user from the group"""
113+
# Handle some possible errors
114+
if message.sender not in chat.admins:
115+
message.reply("You're not an admin of this chat!")
116+
return
117+
if not message.reply_to_message:
118+
message.reply("You must reply to a message the user sent!")
119+
return
120+
if message.reply_to_message.sender in chat.admins:
121+
message.reply("You can't ban another admin!")
122+
return
123+
124+
# Get the exact ban duration
125+
try:
126+
length = int(args[0])
127+
except (IndexError, TypeError):
128+
message.reply("You must provide the length of the ban!")
129+
return
130+
131+
# Store the ban in the shared memory
132+
with shared.lock("bans-change"):
133+
bans = shared["bans"]
134+
if chat.id not in bans:
135+
bans[chat.id] = {}
136+
137+
# Calculate the expiry time and store it
138+
expiry = time.time()+length*BAN_DURATION_MULTIPLIER
139+
bans[chat.id][message.reply_to_message.sender.id] = expiry
140+
shared["bans"] = bans
141+
142+
# Let's finally ban this guy!
143+
chat.ban(message.reply_to_message.sender)
144+
145+
@bot.timer(BAN_DURATION_MULTIPLIER)
146+
def unban_timer(bot, shared):
147+
"""Unban the users"""
148+
now = time.time()
149+
150+
# Everything is locked so no there are no races
151+
with shared.lock("bans-change"):
152+
global_bans = shared["bans"]
153+
154+
# Here .copy() is used because we're changing the dicts in-place
155+
for chat_id, bans in global_bans.copy().items():
156+
for user, expires in bans.copy().items():
157+
# Unban the user if his ban expired
158+
if expires <= now:
159+
continue
160+
bot.chat(chat_id).unban(user)
161+
del global_bans[chat_id][user]
162+
163+
# Cleaning up is not such a bad thing...
164+
if not global_bans[chat_id]:
165+
del global_bans[chat_id]
166+
167+
shared["bans"] = global_bans

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Narrative documentation
5555
bot-creation
5656
bot-structure
5757
unavailable-chats
58+
groups-management
5859
shared-memory
5960
tasks
6061
custom-components

0 commit comments

Comments
 (0)