Initial commit
8
.env.local
Normal file
@ -0,0 +1,8 @@
|
||||
bot_token=
|
||||
firebase_apiKey=
|
||||
firebase_authDomain=
|
||||
firebase_databaseURL=
|
||||
firebase_projectId=
|
||||
firebase_storageBucket=
|
||||
app_url=
|
||||
PORT=
|
129
.gitignore
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Miłosz Guglas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
67
README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# <p align="center"> [everyone-mention-telegram-bot](http://t.me/everyone_mention_bot)
|
||||
<p align="center"> <img src="docs/logo.png" width="150"/>
|
||||
<p align="center"> simple, but useful telegram bot to gather all of group members attention!
|
||||
<!-- Icon made by https://www.freepik.com from https://www.flaticon.com/ -->
|
||||
|
||||
## Contents
|
||||
|
||||
* [Getting started.](#getting-started)
|
||||
* [Installation](#installation)
|
||||
* [Requirements](#requirements)
|
||||
* [Env file](#env-file)
|
||||
* [Commands](#commands)
|
||||
* [`/in`](#in)
|
||||
* [`/out`](#out)
|
||||
* [`/everyone`](#everyone)
|
||||
|
||||
### Getting started
|
||||
#### Installation
|
||||
```bash
|
||||
git clone https://github.com/miloszowi/everyone-mention-telegram-bot.git
|
||||
pip install -r requirements.txt
|
||||
python entrypoint.py
|
||||
```
|
||||
|
||||
#### Requirements
|
||||
- `python` with version specified in `runtime.txt`
|
||||
- `pip` with version `20.0.2`
|
||||
|
||||
#### Env file
|
||||
```bash
|
||||
cp .env.local .env
|
||||
```
|
||||
and then fulfill copied `.env` file with required values
|
||||
- `bot_token` - your telegram bot token from [BotFather](https://telegram.me/BotFather)
|
||||
- `firebase_*` - all of those values you can find in firebase console
|
||||
- `app_url` - your app url for retrieving webhooks
|
||||
- `PORT` - port for your app
|
||||
|
||||
### Commands
|
||||
#### `/in`
|
||||
Will sign you in for everyone-mentions.
|
||||
|
||||

|
||||
|
||||
If you have already opted-in before, alternative reply will be displayed.
|
||||
|
||||

|
||||
|
||||
#### `/out`
|
||||
Will sign you off for everyone-mentions.
|
||||
|
||||

|
||||
|
||||
If you haven't opted-in before, alternative reply will be displayed.
|
||||
|
||||

|
||||
|
||||
#### `/everone`
|
||||
Will mention everyone that opted-in for everyone-mentions separated by spaces.
|
||||
|
||||
If user does not contain nickname, his ID will be present instead of nickname.
|
||||
|
||||

|
||||
|
||||
If there are no users that opted-in for mentioning, alternative reply will be displayed.
|
||||
|
||||

|
BIN
docs/commands.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/everyone_command.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/everyone_noone_to_mention.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
docs/in_command.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/in_command_already_opted_in.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
docs/logo.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
docs/out_command.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
docs/out_command_did_not_opt_in_before.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
6
entrypoint.py
Normal file
@ -0,0 +1,6 @@
|
||||
from src.app import App
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = App()
|
||||
|
||||
app.run()
|
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
python-dotenv==0.19.0
|
||||
python-telegram-bot==13.7
|
||||
Pyrebase==3.0.27
|
1
runtime.txt
Normal file
@ -0,0 +1 @@
|
||||
python-3.8.10
|
34
src/app.py
Normal file
@ -0,0 +1,34 @@
|
||||
from .config.credentials import bot_token, app_url, port
|
||||
from .config.handlers import handlers
|
||||
from .handlers.handlerInterface import HandlerInterface
|
||||
from telegram.ext.dispatcher import Dispatcher
|
||||
from telegram.ext import Updater
|
||||
|
||||
|
||||
class App:
|
||||
updater: Updater
|
||||
dispatcher: Dispatcher
|
||||
|
||||
def __init__(self):
|
||||
self.updater = Updater(bot_token)
|
||||
|
||||
def run(self) -> None:
|
||||
self.registerHandlers()
|
||||
self.registerWebhook()
|
||||
|
||||
self.updater.idle()
|
||||
|
||||
def registerHandlers(self) -> None:
|
||||
for handler in handlers:
|
||||
if not isinstance(handler, HandlerInterface):
|
||||
raise Exception('Invalid list of handlers provided. Handler must implement HandlerInterface')
|
||||
|
||||
self.updater.dispatcher.add_handler(handler.getBotHandler())
|
||||
|
||||
def registerWebhook(self) -> None:
|
||||
self.updater.start_webhook(
|
||||
listen="0.0.0.0",
|
||||
port=int(port),
|
||||
url_path=bot_token,
|
||||
webhook_url=f'{app_url}/{bot_token}'
|
||||
)
|
8
src/config/contents.py
Normal file
@ -0,0 +1,8 @@
|
||||
import re
|
||||
|
||||
# These are MarkdownV2 python-telegram-bot specific
|
||||
opted_in_successfully = re.escape('You have opted-in for everyone-mentions.')
|
||||
opted_in_failed = re.escape('You already opted-in for everyone-mentions.')
|
||||
opted_off_successfully = re.escape('You have opted-off for everyone-mentions.')
|
||||
opted_off_failed = re.escape('You need to opt-in first before processing this command.')
|
||||
mention_failed = re.escape('There are no users to mention.')
|
16
src/config/credentials.py
Normal file
@ -0,0 +1,16 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
bot_token = os.environ['bot_token']
|
||||
app_url = os.environ['app_url']
|
||||
port = os.environ['PORT']
|
||||
|
||||
firebaseConfig = {
|
||||
"apiKey": os.environ['firebase_apiKey'],
|
||||
"authDomain": os.environ['firebase_authDomain'],
|
||||
"databaseURL": os.environ['firebase_databaseURL'],
|
||||
"projectId": os.environ['firebase_projectId'],
|
||||
"storageBucket": os.environ['firebase_storageBucket'],
|
||||
}
|
9
src/config/handlers.py
Normal file
@ -0,0 +1,9 @@
|
||||
from ..handlers.inHandler import InHandler
|
||||
from ..handlers.outHandler import OutHandler
|
||||
from ..handlers.mentionHandler import MentionHandler
|
||||
|
||||
handlers = [
|
||||
InHandler(),
|
||||
OutHandler(),
|
||||
MentionHandler()
|
||||
]
|
34
src/firebaseProxy.py
Normal file
@ -0,0 +1,34 @@
|
||||
import pyrebase
|
||||
from pyrebase.pyrebase import Database as FirebaseDB
|
||||
from .config.credentials import firebaseConfig
|
||||
|
||||
|
||||
class FirebaseProxy():
|
||||
db: FirebaseDB
|
||||
|
||||
# Group specific values
|
||||
group_index: str = 'groups'
|
||||
|
||||
# User specific values
|
||||
id_index: str = 'id'
|
||||
name_index: str = 'name'
|
||||
|
||||
def __init__(self) -> None:
|
||||
firebase = pyrebase.pyrebase.initialize_app(firebaseConfig)
|
||||
self.db = firebase.database()
|
||||
|
||||
def getChilds(self, *childs: str) -> FirebaseDB:
|
||||
current = self.db
|
||||
|
||||
for child_index in childs:
|
||||
current = current.child(child_index)
|
||||
|
||||
return current
|
||||
|
||||
@staticmethod
|
||||
def getGroupPath(groupId: int) -> str:
|
||||
return f'{FirebaseProxy.group_index}/{groupId}'
|
||||
|
||||
@staticmethod
|
||||
def getUserPath(userId: int, groupId: int) -> str:
|
||||
return f'{groupId}_{userId}'
|
18
src/handlers/handlerInterface.py
Normal file
@ -0,0 +1,18 @@
|
||||
from abc import abstractmethod
|
||||
from telegram.ext.callbackcontext import CallbackContext
|
||||
from telegram.ext.handler import Handler
|
||||
from telegram.update import Update
|
||||
|
||||
|
||||
class HandlerInterface:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def getBotHandler(self) -> Handler: raise Exception('getBotHandler method is not implemented')
|
||||
|
||||
@abstractmethod
|
||||
def handle(self, update: Update, context: CallbackContext) -> None: raise Exception('handle method is not implemented')
|
||||
|
||||
@abstractmethod
|
||||
def getCommandName(self) -> str: raise Exception('getCommandName method is not implemented')
|
39
src/handlers/inHandler.py
Normal file
@ -0,0 +1,39 @@
|
||||
from ..config.contents import opted_in_successfully, opted_in_failed
|
||||
from ..repositories.userRepository import UserRepository
|
||||
from ..firebaseProxy import FirebaseProxy
|
||||
from .handlerInterface import HandlerInterface
|
||||
from telegram.ext.callbackcontext import CallbackContext
|
||||
from telegram.ext.commandhandler import CommandHandler
|
||||
from telegram.update import Update
|
||||
|
||||
|
||||
class InHandler(HandlerInterface):
|
||||
botHandler: CommandHandler
|
||||
commandName: str = 'in'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.botHandler = CommandHandler(
|
||||
self.getCommandName(),
|
||||
self.handle
|
||||
)
|
||||
|
||||
def handle(self, update: Update, context: CallbackContext) -> None:
|
||||
groupId = update.effective_chat.id
|
||||
userData = {
|
||||
FirebaseProxy.id_index: update.effective_user.id,
|
||||
FirebaseProxy.name_index: update.effective_user.username
|
||||
}
|
||||
userRepository = UserRepository()
|
||||
|
||||
if userRepository.isPresentInGroup(userData.get(FirebaseProxy.id_index), groupId):
|
||||
update.message.reply_markdown_v2(text=opted_in_failed)
|
||||
return
|
||||
|
||||
userRepository.addForGroup(userData, groupId)
|
||||
update.message.reply_markdown_v2(text=opted_in_successfully)
|
||||
|
||||
def getBotHandler(self) -> CommandHandler:
|
||||
return self.botHandler
|
||||
|
||||
def getCommandName(self) -> str:
|
||||
return self.commandName
|
42
src/handlers/mentionHandler.py
Normal file
@ -0,0 +1,42 @@
|
||||
from ..config.contents import mention_failed
|
||||
from ..firebaseProxy import FirebaseProxy
|
||||
from ..repositories.groupRepository import GroupRepository
|
||||
from .handlerInterface import HandlerInterface
|
||||
from telegram.ext.callbackcontext import CallbackContext
|
||||
from telegram.ext.commandhandler import CommandHandler
|
||||
from telegram.update import Update
|
||||
|
||||
|
||||
class MentionHandler(HandlerInterface):
|
||||
botHandler: CommandHandler
|
||||
commandName: str = 'everyone'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.botHandler = CommandHandler(
|
||||
self.getCommandName(),
|
||||
self.handle
|
||||
)
|
||||
|
||||
def handle(self, update: Update, context: CallbackContext) -> None:
|
||||
groupId = update.effective_chat.id
|
||||
groupRepository = GroupRepository()
|
||||
mentionMessage = self.buildMentionMessage(groupRepository.get(id=groupId))
|
||||
|
||||
update.message.reply_markdown_v2(text=mentionMessage)
|
||||
|
||||
def getBotHandler(self) -> CommandHandler:
|
||||
return self.botHandler
|
||||
|
||||
def getCommandName(self) -> str:
|
||||
return self.commandName
|
||||
|
||||
def buildMentionMessage(self, usersData: dict) -> str:
|
||||
result = ''
|
||||
|
||||
for userData in usersData:
|
||||
userId = str(userData.get(FirebaseProxy.id_index))
|
||||
username = userData.get(FirebaseProxy.name_index) or userId
|
||||
|
||||
result += "*[%s](tg://user?id=%s)* " % (username, userId)
|
||||
|
||||
return result or mention_failed
|
39
src/handlers/outHandler.py
Normal file
@ -0,0 +1,39 @@
|
||||
from ..config.contents import opted_off_successfully, opted_off_failed
|
||||
from ..repositories.userRepository import UserRepository
|
||||
from .handlerInterface import HandlerInterface
|
||||
from telegram.ext.callbackcontext import CallbackContext
|
||||
from telegram.ext.commandhandler import CommandHandler
|
||||
from telegram.update import Update
|
||||
|
||||
|
||||
class OutHandler(HandlerInterface):
|
||||
botHandler: CommandHandler
|
||||
commandName: str = 'out'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.botHandler = CommandHandler(
|
||||
self.getCommandName(),
|
||||
self.handle
|
||||
)
|
||||
|
||||
def handle(self, update: Update, context: CallbackContext) -> None:
|
||||
groupId = update.effective_chat.id
|
||||
userData = {
|
||||
'id': update.effective_user.id,
|
||||
'name': update.effective_user.username
|
||||
}
|
||||
|
||||
userRepository = UserRepository()
|
||||
if not userRepository.isPresentInGroup(userData.get('id'), groupId):
|
||||
update.message.reply_markdown_v2(text=opted_off_failed)
|
||||
return
|
||||
|
||||
userRepository.removeForGroup(userId=userData.get('id'), groupId=groupId)
|
||||
|
||||
update.message.reply_markdown_v2(text=opted_off_successfully)
|
||||
|
||||
def getBotHandler(self) -> CommandHandler:
|
||||
return self.botHandler
|
||||
|
||||
def getCommandName(self) -> str:
|
||||
return self.commandName
|
18
src/repositories/groupRepository.py
Normal file
@ -0,0 +1,18 @@
|
||||
from ..firebaseProxy import FirebaseProxy
|
||||
|
||||
|
||||
class GroupRepository():
|
||||
firebase: FirebaseProxy
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.firebase = FirebaseProxy()
|
||||
|
||||
def get(self, id: int) -> dict:
|
||||
result = []
|
||||
groupData = self.firebase.getChilds(FirebaseProxy.group_index, id).get()
|
||||
|
||||
if groupData.each():
|
||||
for user_root in groupData.each():
|
||||
result.append(user_root.val())
|
||||
|
||||
return result
|
30
src/repositories/userRepository.py
Normal file
@ -0,0 +1,30 @@
|
||||
from ..firebaseProxy import FirebaseProxy
|
||||
|
||||
|
||||
class UserRepository():
|
||||
firebaseProxy: FirebaseProxy
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.firebaseProxy = FirebaseProxy()
|
||||
|
||||
def addForGroup(self, userData: dict, groupId: int) -> None:
|
||||
self.firebaseProxy.getChilds(FirebaseProxy.getGroupPath(groupId)).update({
|
||||
f'{groupId}_{userData.get("id")}': {
|
||||
FirebaseProxy.id_index: userData.get("id"),
|
||||
FirebaseProxy.name_index: userData.get("name")
|
||||
}
|
||||
})
|
||||
|
||||
def removeForGroup(self, userId: int, groupId: int) -> None:
|
||||
self.firebaseProxy.getChilds(FirebaseProxy.getGroupPath(groupId)).update({
|
||||
FirebaseProxy.getUserPath(userId, groupId): {}
|
||||
})
|
||||
|
||||
def isPresentInGroup(self, userId: int, groupId: int) -> bool:
|
||||
user = self.firebaseProxy.getChilds(
|
||||
FirebaseProxy.getGroupPath(groupId),
|
||||
FirebaseProxy.getUserPath(userId, groupId)
|
||||
).get().val()
|
||||
|
||||
return bool(user)
|
||||
|