├── .gitignore ├── requirements.txt ├── scripts └── setup-devenv.sh ├── README.md ├── set_interval.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-telegram-bot==8.0.0 -------------------------------------------------------------------------------- /scripts/setup-devenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echoerr() { 4 | >&2 echo "$@" 5 | } 6 | 7 | he_she_said_no() { 8 | [[ "$@" = "n" ]] || [[ "$@" = "N" ]] 9 | } 10 | 11 | root=$(dirname $(dirname $(realpath $0))) 12 | 13 | cd $root 14 | if [ ! -d "venv" ]; then 15 | read -p "Virtualenv not found, create? [Y/n] " create_venv 16 | if he_she_said_no $create_venv; then 17 | exit 0 18 | fi 19 | if ! command -v virtualenv &> /dev/null 20 | then 21 | read -p "virtualenv command missing, try to install? (debian based distros supported) [Y/n] " install_virtualenv 22 | if he_she_said_no $install_virtualenv; then 23 | exit 0 24 | fi 25 | sudo apt-get update && sudo apt-get install --no-install-recommends -y python3-virtualenv 26 | fi 27 | virtualenv -p python3 venv 1>&2 28 | ./venv/bin/pip3 install -r requirements.txt 1>&2 29 | fi 30 | 31 | echo "source $root/venv/bin/activate" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This is a joke between friends to make annoying reminders. The user (master) will give the bot a message to spam, then the bot will send a message every `/spamint` seconds and delete it after `/delint` seconds since sent. Multiple messages can be configured, each having independent interval settings. 4 | 5 | The dependencies are ancient: the code has not been updated since sept, 2017. 6 | 7 | ## Development Environment 8 | 9 | Activate (setup if is the first time) the virtual env: 10 | 11 | ```bash 12 | $(./scripts/setup-devenv.sh) 13 | ``` 14 | 15 | This will install virtualenv and dependencies listed in requirements.txt. Then it will source it. 16 | If dependencies are already installed, this will only activate the existing virtualenv. 17 | 18 | ## Run 19 | 20 | With virtualenv activated run: 21 | 22 | ```bash 23 | (venv) SPAMBOT_TOKEN='your-bot-token-here' python main.py 24 | ``` 25 | 26 | ### Example 27 | 28 | Start a chat with the bot then: 29 | 30 | 1. Case 1: Simple message with default intervals (spam interval 5s, delete interval 1s) 31 | 32 | ``` 33 | you: /spam simple 34 | # second 0 35 | bot: simple 36 | # second: 1 37 | bot: *deletes message* 38 | # second 5 39 | bot: simple 40 | # repeat 41 | ``` 42 | 43 | 2. Case 2: Larger message with id and custom intervals 44 | 45 | ``` 46 | you: /spam some big message #big 47 | # second 0 48 | bot: some big message 49 | you: /spamint #big 10 50 | bot: New interval set for #big: 10.0 seconds 51 | you: /delint #big 25 52 | bot: New interval set for #big: 10.0 seconds 53 | # now bot will send every 10 seconds and delete after 25 (overlap) 54 | ``` 55 | 56 | To stop messages, run: 57 | 58 | ``` 59 | you: /stop simple 60 | you: /stop #big 61 | ``` -------------------------------------------------------------------------------- /set_interval.py: -------------------------------------------------------------------------------- 1 | from threading import Timer 2 | 3 | 4 | class Interval: 5 | def __init__(self, func, secs): 6 | # Fuction to be called back 7 | self.func = func 8 | # Interval 9 | self.secs = secs 10 | # Called back after calling self.func 11 | self.refresh_cb = None 12 | # if 0, it won't join 13 | self.join_timeout = 0 14 | # Start right away 15 | self._refresh_interval() 16 | 17 | def _refresh_interval(self): 18 | # Call function 19 | self.func() 20 | # Refresh timer object 21 | self.timer = Timer(self.secs, self._refresh_interval) 22 | self.timer.start() 23 | # Call control function if set 24 | if self.refresh_cb is not None: 25 | self.refresh_cb(self) 26 | # Join thread if specified 27 | if self.join_timeout != 0: 28 | self.join(self.join_timeout) 29 | 30 | def set(self, secs): 31 | ''' 32 | Override set secs on Interval construction 33 | ''' 34 | self.secs = secs 35 | # Cancel previous timer and run new one 36 | self.cancel() 37 | self._refresh_interval() 38 | 39 | def cancel(self): 40 | # Cancel interval 41 | self.timer.cancel() 42 | 43 | def join(self, timeout=None): 44 | # Join interval 45 | self.join_timeout = timeout 46 | self.timer.join(timeout) 47 | 48 | def on_refresh(self, callback): 49 | # Set control function 50 | self.refresh_cb = callback 51 | 52 | 53 | if __name__ == '__main__': 54 | def test_func(): 55 | print('test_func') 56 | 57 | class TestClass: 58 | def __call__(self): 59 | print('TestClass') 60 | 61 | interval = Interval(TestClass(), 1) 62 | 63 | def stop_after_six(i): 64 | if not hasattr(i, 'sas_count'): 65 | i.sas_count = 1 66 | 67 | if i.sas_count == 5: 68 | i.cancel() 69 | 70 | i.sas_count += 1 71 | 72 | interval.on_refresh(stop_after_six) 73 | interval.join() 74 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from telegram.ext import Updater, CommandHandler 2 | from telegram.error import BadRequest 3 | from set_interval import Interval 4 | from threading import Timer, Lock 5 | import logging 6 | import os 7 | 8 | # logging config 9 | logging.basicConfig( 10 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 11 | level=logging.INFO 12 | ) 13 | 14 | DEFAULT_INTERVAL = 5 # seconds 15 | DEFAULT_DELETE_TIME = 1 16 | 17 | 18 | class Message: 19 | def __init__(self, id, on_delete, delete_time): 20 | self.id = id 21 | self.on_delete = on_delete 22 | self.restart(delete_time) 23 | 24 | def restart(self, delete_time): 25 | # If timer exists, cancel it 26 | if hasattr(self, 'timer'): 27 | self.stop() 28 | # Reset timer 29 | self.timer = Timer(delete_time, 30 | self.on_delete, (self.id,)) 31 | self.timer.start() 32 | 33 | def stop(self): 34 | self.timer.cancel() 35 | 36 | 37 | class Spam: 38 | def __init__(self, id, user_id, spam_msg, msg_id, bot): 39 | self.id = id 40 | self.creator = user_id 41 | self.spam_msg = spam_msg 42 | self.msg_id = msg_id 43 | self.bot = bot 44 | self.delete_time = DEFAULT_DELETE_TIME 45 | self.messages = { 46 | # message_id: Message 47 | } 48 | self.msgs_mutex = Lock() 49 | 50 | # Send when registered 51 | self.interval = Interval(self.send_message, 52 | DEFAULT_INTERVAL) 53 | 54 | def send_message(self): 55 | self.msgs_mutex.acquire() 56 | m = self.bot.send_message(chat_id=self.id, text=self.spam_msg) 57 | self.messages[m.message_id] = Message( 58 | m.message_id, self.delete_message, self.delete_time) 59 | self.msgs_mutex.release() 60 | 61 | def delete_message(self, message_id, lock=True): 62 | try: 63 | self.bot.delete_message(chat_id=self.id, message_id=message_id) 64 | except BadRequest: 65 | logging.log(logging.ERROR, 'BadRequest while deleting message!') 66 | 67 | # Delete from register 68 | if lock: 69 | self.msgs_mutex.acquire() 70 | if message_id in self.messages: 71 | del self.messages[message_id] 72 | if lock: 73 | self.msgs_mutex.release() 74 | 75 | def set_spam_interval(self, secs): 76 | self.interval.set(secs) 77 | 78 | def set_delete_interval(self, secs): 79 | self.delete_time = secs 80 | self.delete_messages() 81 | 82 | def get_key(self): 83 | return (self.id, self.spam_msg) 84 | 85 | def get_short_key(self): 86 | if self.msg_id is None: 87 | return 88 | return (self.id, self.creator, 89 | self.msg_id) 90 | 91 | def delete_messages(self): 92 | self.msgs_mutex.acquire() 93 | # Avoid delete while iterating 94 | while len(self.messages) > 0: 95 | message = list(self.messages.values())[0] 96 | message.stop() 97 | self.delete_message(message.id, 98 | lock=False) 99 | self.msgs_mutex.release() 100 | 101 | def stop(self): 102 | self.delete_messages() 103 | self.interval.cancel() 104 | 105 | 106 | spams = { 107 | # id: Spam 108 | } 109 | 110 | 111 | def spam(bot, update): 112 | spam_msg = update.message.text.split('/spam')[1].strip() 113 | if spam_msg == '': 114 | update.message.reply_text('Give me a message!') 115 | return 116 | 117 | # Get msg_id 118 | spam_split = spam_msg.split('#') 119 | msg_id = None 120 | 121 | if len(spam_split) > 1: 122 | # Last after # is msg_id 123 | msg_id = spam_split[-1].strip() 124 | spam_msg = '#'.join(spam_split[:-1]) 125 | 126 | # Get chat_id 127 | chat_id = update.message.chat_id 128 | user_id = update.message.from_user.id 129 | 130 | # Generate long key 131 | spam_key = (chat_id, spam_msg) 132 | # Generate short key 133 | short_key = None 134 | 135 | if msg_id is not None: 136 | short_key = (chat_id, user_id, msg_id) 137 | 138 | # Create the chat object 139 | spam = Spam(chat_id, user_id, spam_msg, msg_id, bot) 140 | spams[spam_key] = spam 141 | 142 | if short_key is not None: 143 | spams[short_key] = spam 144 | 145 | 146 | def stop(bot, update): 147 | arg = update.message.text.split('/stop')[1].strip() 148 | if arg == '': 149 | update.message.reply_text('Which message?') 150 | return 151 | 152 | # Get chat_id 153 | chat_id = update.message.chat_id 154 | user_id = update.message.from_user.id 155 | 156 | if arg.startswith('#'): 157 | key = (chat_id, user_id, arg[1:].strip()) 158 | else: 159 | key = (chat_id, arg) 160 | 161 | if key in spams: 162 | spam = spams[key] 163 | 164 | if spam.creator == user_id: 165 | spam.stop() 166 | 167 | # Delete both references if necessary 168 | spam_key = spam.get_key() 169 | short_key = spam.get_short_key() 170 | 171 | del spams[spam_key] 172 | if short_key is not None: 173 | del spams[short_key] 174 | 175 | update.message.reply_text("As you request, master.") 176 | else: 177 | update.message.reply_text('You are not my creator!') 178 | else: 179 | update.message.reply_text('No such spam!') 180 | 181 | 182 | def get_positive_number_or_none(s): 183 | try: 184 | f = float(s) 185 | # Check not NaN 186 | if f == float('NaN'): 187 | return 188 | elif f <= 0: 189 | return 190 | 191 | return f 192 | except ValueError: 193 | return 194 | 195 | 196 | def set_interval(bot, update, comm, method): 197 | args = update.message.text.split(comm) 198 | if len(args) == 1: 199 | logging.log(logging.ERROR, 'Invalid method: %s' % method) 200 | else: 201 | args = args[1].strip() 202 | 203 | if args == '': 204 | update.message.reply_text( 205 | 'Usage: {} (#message_id|message) interval'.format(comm)) 206 | return 207 | 208 | args_split = args.split() 209 | if len(args_split) <= 1: 210 | update.message.reply_text( 211 | 'Usage: {} (#message_id|message) interval'.format(comm)) 212 | return 213 | 214 | interval = get_positive_number_or_none(args_split[-1].strip()) 215 | 216 | if interval == None: 217 | update.message.reply_text('Invalid interval!') 218 | return 219 | 220 | # Get chat_id 221 | chat_id = update.message.chat_id 222 | user_id = update.message.from_user.id 223 | 224 | spam_s = ''.join(args_split[:-1]).strip() 225 | if spam_s.startswith('#'): 226 | key = (chat_id, user_id, spam_s[1:].strip()) 227 | else: 228 | key = (chat_id, spam_s) 229 | 230 | if key in spams: 231 | spam = spams[key] 232 | spam_set_interval = getattr(spam, method, None) 233 | if spam_set_interval == None: 234 | logging.log(logging.ERROR, 'Invalid method: %s' % method) 235 | return 236 | 237 | update.message.reply_text( 238 | 'New interval set for {}: {} seconds'.format(spam_s, interval)) 239 | spam_set_interval(interval) 240 | else: 241 | update.message.reply_text('No such spam!') 242 | 243 | 244 | def spam_interval(bot, update): 245 | set_interval(bot, update, '/spamint', 'set_spam_interval') 246 | 247 | 248 | def delete_interval(bot, update): 249 | set_interval(bot, update, '/delint', 'set_delete_interval') 250 | 251 | 252 | if __name__ == '__main__': 253 | # Create updater 254 | tokenvar = 'SPAMBOT_TOKEN' 255 | token = os.getenv(tokenvar, None) 256 | if token is None: 257 | raise RuntimeError('{} must be defined'.format(tokenvar)) 258 | updater = Updater(token) 259 | # Register commands 260 | updater.dispatcher.add_handler(CommandHandler('spam', spam)) 261 | updater.dispatcher.add_handler(CommandHandler('stop', stop)) 262 | updater.dispatcher.add_handler(CommandHandler('spamint', 263 | spam_interval)) 264 | updater.dispatcher.add_handler(CommandHandler('delint', 265 | delete_interval)) 266 | 267 | # Start updater 268 | updater.start_polling() 269 | updater.idle() 270 | --------------------------------------------------------------------------------