diff --git a/requirements.txt b/requirements.txt index 9c96191..3d6982a 100755 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,11 @@ python-telegram-bot==13.1 psycopg2-binary feedparser toml -googletrans +googletrans==4.0.0-rc1 pillow alphabet-detector flask flask-restplus -Werkzeug==0.16.1 -flask-httpauth \ No newline at end of file +Werkzeug==1.0.1 +flask-httpauth +schedule \ No newline at end of file diff --git a/tg_bot/__init__.py b/tg_bot/__init__.py index b82f650..9577eb8 100755 --- a/tg_bot/__init__.py +++ b/tg_bot/__init__.py @@ -22,12 +22,12 @@ import telegram.ext as tg from tg_bot.strings.string_helper import get_string +VERSION = 2.1 + # Module name module = "init" # enable logging - - LOGGER = logging.getLogger(__name__) ENV = bool(os.environ.get('ENV', False)) @@ -49,7 +49,11 @@ format=LOGFORMAT, level=logging.INFO) - +LOGGER.info(f"Nemesis v{VERSION}\n" + f"This program is free software: you can redistribute it and/or modify\n" + f"it under the terms of the GNU General Public License as published by\n" + f"the Free Software Foundation, either version 3 of the License, or\n" + f"(at your option) any later version.") # if version < 3.6, stop bot. if sys.version_info[0] < 3 or sys.version_info[1] < 6: LOGGER.error(get_string(module, "ERR_INVALID_PYTHON_VERSION", DEFAULT_LANG)) # ERR_INVALID_PYTHON_VERSION @@ -152,7 +156,6 @@ SUDO_USERS.add(OWNER_ID) SUDO_USERS.add(CO_OWNER_ID) LOGGER.info("Owner: %s", OWNER_ID ) -LOGGER.info("Co-Owner: %s", CO_OWNER_ID ) updater = tg.Updater(TOKEN, workers=WORKERS, use_context=True) diff --git a/tg_bot/__main__.py b/tg_bot/__main__.py index c5aaf4b..803b055 100644 --- a/tg_bot/__main__.py +++ b/tg_bot/__main__.py @@ -30,7 +30,7 @@ from telegram.utils.helpers import escape_markdown from tg_bot import dispatcher, updater, TOKEN, WEBHOOK, OWNER_ID, DONATION_LINK, CERT_PATH, PORT, URL, LOGGER, \ - ALLOW_EXCL, DEFAULT_LANG + ALLOW_EXCL, DEFAULT_LANG, VERSION from tg_bot.strings.string_helper import get_string @@ -368,6 +368,7 @@ def get_settings(update: Update, context: CallbackContext): else: send_settings(chat.id, user.id, True) + def migrate_chats(update: Update, context: CallbackContext): msg = update.effective_message # type: Optional[Message] if msg.migrate_to_chat_id: @@ -412,7 +413,7 @@ def about(update: Update, context: CallbackContext): " - [Severus Snape](https://t.me/GenosseSeverus) - Co-Owner\n" \ " - [Luna Loony](https://t.me/Luna_loony) - Admin\n" - update.effective_message.reply_text("*Nemesis - Powerful open-source group manager*\n" + update.effective_message.reply_text("*Nemesis v{} - Powerful open-source group manager*\n" "Copyright (C) 2017 - 2019 Paul Larsen\n" "Copyright (C) 2019 - 2020 KaratekHD\n\n" "This program is free software: you can redistribute it and/or modify " @@ -428,7 +429,7 @@ def about(update: Update, context: CallbackContext): "*Translation*\n" "{}" "*Production*\n" - "{}".format(DEVELOPMENT, TRANSLATION, PRODUCTION), parse_mode=ParseMode.MARKDOWN) + "{}".format(VERSION, DEVELOPMENT, TRANSLATION, PRODUCTION), parse_mode=ParseMode.MARKDOWN) def load_api(): diff --git a/tg_bot/modules/admin.py b/tg_bot/modules/admin.py index 2c5f159..3008ae9 100755 --- a/tg_bot/modules/admin.py +++ b/tg_bot/modules/admin.py @@ -53,7 +53,6 @@ def toggle_mute(update: Update, context: CallbackContext) -> str: @user_not_admin def on_message(update: Update, context: CallbackContext): - LOGGER.debug("Yeet!") if mute_sql.get_muted(update.effective_chat.id): update.effective_message.delete() diff --git a/tg_bot/modules/reputations.py b/tg_bot/modules/reputations.py index e8ea046..652e6fb 100644 --- a/tg_bot/modules/reputations.py +++ b/tg_bot/modules/reputations.py @@ -133,10 +133,18 @@ def __user_settings__(user_id): INCREASE_MESSAGE_HANDLER = DisableAbleMessageHandler(Filters.regex(r"^\+$"), increase, friendly="increase", run_async=True) + dispatcher.add_handler(INCREASE_MESSAGE_HANDLER) -INCREASE_MESSAGE_HANDLER = DisableAbleMessageHandler(Filters.regex(r"^\-$"), decrease, friendly="decrease", +DECREASE_MESSAGE_HANDLER = DisableAbleMessageHandler(Filters.regex(r"^\-$"), decrease, friendly="decrease", run_async=True) -dispatcher.add_handler(INCREASE_MESSAGE_HANDLER) +dispatcher.add_handler(DECREASE_MESSAGE_HANDLER) +INCREASE_MESSAGE_HANDLER2 = DisableAbleMessageHandler(Filters.regex(r"^\👍$"), increase, friendly="increase", + run_async=True) + +dispatcher.add_handler(INCREASE_MESSAGE_HANDLER2) +DECREASE_MESSAGE_HANDLER2 = DisableAbleMessageHandler(Filters.regex(r"^\👎$"), decrease, friendly="decrease", + run_async=True) +dispatcher.add_handler(DECREASE_MESSAGE_HANDLER2) SETTINGS_HANDLER = CommandHandler("reputation", reputation, run_async=True) dispatcher.add_handler(SETTINGS_HANDLER) diff --git a/tg_bot/modules/sql/api_sql.py b/tg_bot/modules/sql/api_sql.py index 54812df..c870f37 100644 --- a/tg_bot/modules/sql/api_sql.py +++ b/tg_bot/modules/sql/api_sql.py @@ -15,10 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import threading from sqlalchemy import Column, String, UnicodeText, func, distinct, Integer -from werkzeug.security import generate_password_hash, check_password_hash from tg_bot.modules.sql import SESSION, BASE @@ -37,6 +35,7 @@ def __repr__(self): Api.__table__.create(checkfirst=True) + def set_key(user_id, key): user = SESSION.query(Api).get(user_id) if not user: @@ -49,10 +48,10 @@ def set_key(user_id, key): def verify_key(key): - hash = SESSION.query(Api).get(Api.key == str(key)) - ret = "null" - if hash: - ret = hash.user_id + result = SESSION.query(Api).filter(Api.key == str(key)).scalar() + ret = None + if result: + ret = result.user_id SESSION.close() return ret diff --git a/tg_bot/modules/sql/users_sql.py b/tg_bot/modules/sql/users_sql.py index b664996..d98c582 100755 --- a/tg_bot/modules/sql/users_sql.py +++ b/tg_bot/modules/sql/users_sql.py @@ -88,6 +88,13 @@ def ensure_bot_in_db(): SESSION.commit() +def get_chatname(chat_id): + try: + return SESSION.query(Chats).get(str(chat_id)).chat_name + finally: + SESSION.close() + + def update_user(user_id, username, chat_id=None, chat_name=None): with INSERTION_LOCK: user = SESSION.query(Users).get(user_id) @@ -141,6 +148,13 @@ def get_chat_members(chat_id): SESSION.close() +def get_chats_by_member(user_id): + try: + return SESSION.query(ChatMembers).filter(ChatMembers.user == str(user_id)).all() + finally: + SESSION.close() + + def get_all_chats(): try: return SESSION.query(Chats).all() @@ -148,6 +162,13 @@ def get_all_chats(): SESSION.close() +def get_all_users(): + try: + return SESSION.query(Users).all() + finally: + SESSION.close() + + def get_user_num_chats(user_id): try: return SESSION.query(ChatMembers).filter(ChatMembers.user == int(user_id)).count() diff --git a/tg_bot/modules/stickers.py b/tg_bot/modules/stickers.py index 43951fc..c5b8df7 100644 --- a/tg_bot/modules/stickers.py +++ b/tg_bot/modules/stickers.py @@ -170,6 +170,9 @@ def kang(update, context): print(e) else: + update.effective_message.reply_text("Kanging animated stickers is not supported, see" + " [#57](https://github.com/KaratekHD/Nemesis/issues/57)", parse_mode=ParseMode.MARKDOWN) + return packname = "animated" + str(user.id) + "_by_" + context.bot.username packname_found = 0 max_stickers = 50 @@ -304,7 +307,7 @@ def kang(update, context): elif e.message == "Invalid sticker emojis": msg.reply_text("Invalid emoji(s).") elif e.message == "Stickers_too_much": - msg.reply_text("Max packsize reached. Press F to pay respecc.") + msg.reply_text("Max packsize reached. Press F to pay respect.") elif e.message == "Internal Server Error: sticker set not found (500)": msg.reply_text( "Sticker successfully added to [pack](t.me/addstickers/%s)" @@ -466,6 +469,8 @@ def __help__(update: Update) -> str: """ __mod_name__ = "Stickers" + + KANG_HANDLER = DisableAbleCommandHandler("kang", kang, pass_args=True, admin_ok=True, run_async=True) STICKERID_HANDLER = DisableAbleCommandHandler("stickerid", stickerid, run_async=True) GETSTICKER_HANDLER = DisableAbleCommandHandler("getsticker", getsticker, run_async=True) diff --git a/tg_bot/restapi/__init__.py b/tg_bot/restapi/__init__.py index e0b773a..2ee6196 100644 --- a/tg_bot/restapi/__init__.py +++ b/tg_bot/restapi/__init__.py @@ -19,9 +19,13 @@ from flask_restplus import Api from tg_bot.restapi.resources.basic import basic_api from tg_bot.restapi.resources.chats import chats_api +import tg_bot.restapi.resources.management as management +import tg_bot.restapi.resources.modules.admin as admin app = Flask("Nemesis Telegram Bot") api = Api(app, version="2.0 Development Preview 1", title="Nemesis Telegram Bot") api.add_namespace(basic_api) api.add_namespace(chats_api) +api.add_namespace(management.api) +api.add_namespace(admin.api) \ No newline at end of file diff --git a/tg_bot/restapi/auth.py b/tg_bot/restapi/auth.py index be1c477..9287a2e 100644 --- a/tg_bot/restapi/auth.py +++ b/tg_bot/restapi/auth.py @@ -14,32 +14,9 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from functools import wraps - -from flask import request, abort import tg_bot.modules.sql.api_sql as sql -def token_required(view_function): - @wraps(view_function) - # the new, post-decoration function. Note *args and **kwargs here. - def decorated_function(*args, **kwargs): - if request.args.get('key') and request.args.get('key') == "test": - return view_function(*args, **kwargs) - else: - abort(401) - return decorated_function - -def authenticate(username, password): - if username and password: - key = sql.get_key(username) - if key != "null": - if password is key: - return True - else: - return False - else: - return False - else: - return False \ No newline at end of file +def verify_auth_token(token): + sql.verify_key(token) diff --git a/tg_bot/restapi/models/chats.py b/tg_bot/restapi/models/chats.py new file mode 100644 index 0000000..c1e3f93 --- /dev/null +++ b/tg_bot/restapi/models/chats.py @@ -0,0 +1,25 @@ +# Nemesis - Powerful Telegram group managment bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from flask_restplus import fields + + +def create_chat_model(api): + model = api.model('Chats', { + 'id': fields.String(description="unique group identifier", required=True), + 'name': fields.String(description="group name", required=True) + }) + return model diff --git a/tg_bot/restapi/models/management.py b/tg_bot/restapi/models/management.py new file mode 100644 index 0000000..4b56ec3 --- /dev/null +++ b/tg_bot/restapi/models/management.py @@ -0,0 +1,64 @@ +# Nemesis - Powerful Telegram group managment bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from flask_restplus import fields + +from tg_bot.restapi.models.users import create_user_model + + +def create_chatnumber_model(api): + model = api.model('Total Chats', { + 'number': fields.Integer(description="Number of group the bot has registered.", required=True) + }) + return model + + +def create_countusers_model(api): + model = api.model('Total Users', { + 'number': fields.Integer(description="Number of users the bot has registered.", required=True) + }) + return model + + +def create_chat_model(api): + model = api.model('Chat', { + 'id': fields.Integer(description="Telegram ID of chat", required=True), + 'name': fields.String(description="Name of the group", required=True) + }) + return model + + +def create_chatlist_model(api): + model = api.model('Chats', { + 'chats': fields.Nested(create_chat_model(api), description="list of chats", required=True) + }) + return model + + +def create_broadcast_model(api): + model = api.model("Broadcast", { + 'message': fields.String(description="The message that was broadcasted.", required=True), + "failed": fields.Integer(description="Number of chats that failed receiving the broadcast.", required=True), + "failed_chats": fields.Nested(create_chat_model(api), description="Chats that failed receiving the broadcast.", required=False) + }) + return model + + +def create_userlist_model(api): + model = api.model('Users', { + 'users': fields.Nested(create_user_model(api), description="list of users", required=True) + }) + return model \ No newline at end of file diff --git a/tg_bot/restapi/models/modules/__init__.py b/tg_bot/restapi/models/modules/__init__.py new file mode 100644 index 0000000..1e15628 --- /dev/null +++ b/tg_bot/restapi/models/modules/__init__.py @@ -0,0 +1,17 @@ +# Nemesis - Powerful Telegram group managment bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/tg_bot/restapi/models/modules/admin.py b/tg_bot/restapi/models/modules/admin.py new file mode 100644 index 0000000..1420b75 --- /dev/null +++ b/tg_bot/restapi/models/modules/admin.py @@ -0,0 +1,31 @@ +# Nemesis - Powerful Telegram group managment bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from flask_restplus import fields + +from tg_bot.restapi.models import users + + +def create_chat_model(api): + model = api.model("Chat", { + "id": fields.Integer(description="Telegram ID of group", required=True), + "name": fields.String(description="Name of group", required=True), + "is_muted": fields.Boolean(description="Global mute status", required=True), + # "link": fields.String(description="Convenience property. If username is available, returns a t.me link of the user.", required=False), + "admins": fields.Nested(users.create_user_model(api), description="Administrators", required=False) + }) + return model \ No newline at end of file diff --git a/tg_bot/restapi/models/users.py b/tg_bot/restapi/models/users.py new file mode 100644 index 0000000..49f09f3 --- /dev/null +++ b/tg_bot/restapi/models/users.py @@ -0,0 +1,31 @@ +# Nemesis - Powerful Telegram group managment bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from flask_restplus import fields + +from tg_bot.restapi.models.chats import create_chat_model + + +def create_user_model(api): + model = api.model("User", { + "id": fields.Integer(description="Telegram ID of user", required=True), + "first_name": fields.String(description="First name of user", required=True), + "last_name": fields.String(description="Last name of user", required=False), + "username": fields.String(description="Username", required=False), + "link": fields.String(description="Convenience property. If username is available, returns a t.me link of the user.", required=False), + "groups": fields.Nested(create_chat_model(api), description="All chats the user is part of.", required=False) + }) + return model \ No newline at end of file diff --git a/tg_bot/restapi/resources/chats.py b/tg_bot/restapi/resources/chats.py index 1622293..596d268 100644 --- a/tg_bot/restapi/resources/chats.py +++ b/tg_bot/restapi/resources/chats.py @@ -14,22 +14,45 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json from http import HTTPStatus -from flask import request, jsonify -from flask_httpauth import HTTPBasicAuth +from flask import request, jsonify, abort from flask_restplus import Namespace, Resource +from telegram import TelegramError + import tg_bot.modules.sql.api_sql as sql -from tg_bot import LOGGER +import tg_bot.modules.sql.users_sql as user_sql +from tg_bot import dispatcher +from tg_bot.restapi.models.chats import create_chat_model chats_api = Namespace("chats", description="Gather information about chats you have access to.") +chat = create_chat_model(chats_api) + -@chats_api.route("") -class List(Resource): - def get(self): +@chats_api.route("/") +@chats_api.param('id', 'The group identifier') +@chats_api.response(404, 'Chat not found.') +@chats_api.response(401, "Unauthorized") +@chats_api.response(410, "Bot is not a member of the chat (anymore).") +class Chats(Resource): + @chats_api.marshal_with(chat) + def get(self, id): + '''Gets a chat by id''' key = request.args.get('api_key') - test = sql.verify_key(key) - LOGGER.debug(test) - '''Get All chats you have access to.''' - return "Nemesis Telegram Bot v2.0 Development Preview 1", HTTPStatus.OK + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + user_id = sql.verify_key(key) + chats = [] + for element in user_sql.get_chats_by_member(user_id): + chats.append(element.chat) + if id not in chats: + abort(HTTPStatus.NOT_FOUND, "Chat not found.") + else: + try: + name = dispatcher.bot.get_chat(id).title + except TelegramError as excp: + if excp.message == 'Chat not found': + abort(HTTPStatus.GONE, "Bot is not a member of the chat (anymore).") + return {"id": id, "name": name} diff --git a/tg_bot/restapi/resources/management.py b/tg_bot/restapi/resources/management.py new file mode 100644 index 0000000..c601d93 --- /dev/null +++ b/tg_bot/restapi/resources/management.py @@ -0,0 +1,266 @@ +# Nemesis - Powerful Telegram group management bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from time import sleep + +from flask import request, abort +from flask_restplus import Namespace, Resource +from flask_restplus._http import HTTPStatus +from telegram import TelegramError + +from tg_bot import OWNER_ID, CO_OWNER_ID, SUDO_USERS, dispatcher, LOGGER +import tg_bot.modules.sql.api_sql as sql +from tg_bot.restapi.models.management import * +from tg_bot.restapi.models.users import * +from tg_bot.modules.sql.users_sql import num_chats, get_all_chats, get_all_users, num_users, get_chats_by_member, get_chatname, update_user + +api = Namespace("management", description="Administrative Access") + +chatnumber = create_chatnumber_model(api) + + +@api.route("/countchats") +@api.response(200, "Number attached") +@api.response(403, "User is not bot admin.") +@api.response(401, "Unauthorized") +class NumChats(Resource): + @api.marshal_with(chatnumber) + def get(self): + '''Count all chats from database''' + key = request.args.get('api_key') + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + return + user_id = sql.verify_key(key) + isowner = user_id is OWNER_ID + iscowoner = user_id is CO_OWNER_ID + isudo = user_id in SUDO_USERS + isadmin = isowner or iscowoner or isudo + if not isadmin: + abort(HTTPStatus.FORBIDDEN, "User is not bot admin.") + else: + data = {'number': num_chats()} + return data, HTTPStatus.OK + + +listchats = create_chatlist_model(api) + + +@api.route("/listchats") +@api.response(200, "List attached") +@api.response(403, "User is not bot admin.") +@api.response(401, "Unauthorized") +class ListChats(Resource): + @api.marshal_with(listchats) + def get(self): + '''List all chats from database''' + key = request.args.get('api_key') + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + return + user_id = sql.verify_key(key) + if user_id is None: + abort(HTTPStatus.UNAUTHORIZED, "Invalid Key") + isowner = user_id is OWNER_ID + iscowoner = user_id is CO_OWNER_ID + isudo = user_id in SUDO_USERS + isadmin = isowner or iscowoner or isudo + if not isadmin: + abort(HTTPStatus.FORBIDDEN, "User is not bot admin.") + else: + chats_sql = get_all_chats() + chats = [] + for element in chats_sql: + chats.append([{"id": element.chat_id, "name": element.chat_name}]) + return {"chats": chats} + + +broadcast = create_broadcast_model(api) + + +@api.route("/broadcast") +@api.response(200, "Broadcast completed") +@api.response(403, "User is not bot admin.") +@api.response(401, "Unauthorized") +@api.response(400, "Bad Request") +class Broadcast(Resource): + @api.marshal_with(broadcast) + def post(self): + '''Send a message to all chats''' + key = request.args.get('api_key') + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + return + user_id = sql.verify_key(key) + if user_id is None: + abort(HTTPStatus.UNAUTHORIZED, "Invalid Key") + isowner = user_id is OWNER_ID + iscowoner = user_id is CO_OWNER_ID + isudo = user_id in SUDO_USERS + isadmin = isowner or iscowoner or isudo + if not isadmin: + abort(HTTPStatus.FORBIDDEN, "User is not bot admin.") + else: + data = request.get_json() + try: + to_send = data["message"] + except KeyError: + abort(HTTPStatus.BAD_REQUEST, "Bad Request") + bot = dispatcher.bot + failed_chats = [] + if len(to_send) >= 2: + chats = get_all_chats() or [] + failed = 0 + for chat in chats: + try: + bot.sendMessage(int(chat.chat_id), to_send) + sleep(0.1) + except TelegramError: + failed += 1 + failed_chats.append([{"id": chat.chat_id, "name": chat.chat_name}]) + LOGGER.warning("Couldn't send broadcast to %s, group name %s", str(chat.chat_id), + str(chat.chat_name)) + if failed_chats == 0: + return {"message": to_send, "failed": failed} + else: + return {"message": to_send, "failed": failed, "failed_chats": failed_chats} + + +@api.route("/listusers") +@api.response(403, "User is not bot admin.") +@api.response(401, "Unauthorized") +@api.response(400, "Bad Request") +class ListUsers(Resource): + @api.marshal_with(create_userlist_model(api)) + def get(self): + '''Get a list of all users known to the bot. Note that this does only include people who messaged the bot at least once.''' + key = request.args.get('api_key') + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + return + user_id = sql.verify_key(key) + if user_id is None: + abort(HTTPStatus.UNAUTHORIZED, "Invalid Key") + isowner = user_id is OWNER_ID + iscowoner = user_id is CO_OWNER_ID + isudo = user_id in SUDO_USERS + isadmin = isowner or iscowoner or isudo + if not isadmin: + abort(HTTPStatus.FORBIDDEN, "User is not bot admin.") + else: + users = get_all_users() + result = [] + list_groups_str = request.args.get('list_groups') + if list_groups_str: + if list_groups_str == "True" or list_groups_str == "true": + list_groups = True + if list_groups_str == "False" or list_groups_str == "false": + list_groups = False + if list_groups_str.lower() != "false" and list_groups_str.lower() != "true": + abort(HTTPStatus.BAD_REQUEST, "'list_groups' is invalid.") + else: + list_groups = False + for user in users: + data = {} + try: + data.update({"id": user.user_id}) + chat = dispatcher.bot.get_chat(user.user_id) + if chat.username: + data.update({"username": chat.username}) + data.update({"link": chat.link}) + data.update({"first_name": chat.first_name}) + if chat.last_name: + data.update({"last_name": chat.last_name}) + + if list_groups: + groups = [] + for group in get_chats_by_member(chat.id): + groups.append([{"id": group.chat, "name": get_chatname(group.chat)}]) + data.update({"groups": groups}) + + result.append(data) + except TelegramError as excp: + if excp.message == 'Chat not found': + pass + + + return {"users": result}, HTTPStatus.OK + + +@api.route("/countusers") +@api.response(403, "User is not bot admin.") +@api.response(401, "Unauthorized") +@api.response(400, "Bad Request") +class CountUsers(Resource): + @api.marshal_with(create_countusers_model(api)) + def get(self): + '''Count all users from database''' + key = request.args.get('api_key') + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + return + user_id = sql.verify_key(key) + isowner = user_id is OWNER_ID + iscowoner = user_id is CO_OWNER_ID + isudo = user_id in SUDO_USERS + isadmin = isowner or iscowoner or isudo + if not isadmin: + abort(HTTPStatus.FORBIDDEN, "User is not bot admin.") + else: + data = {'number': num_users()} + return data, HTTPStatus.OK + + +@api.route("/updateuser") +@api.response(403, "User is not bot admin.") +@api.response(401, "Unauthorized") +@api.response(400, "Bad Request") +class UpdateUser(Resource): + def post(self): + '''Update a user inside the db. Use at your own risk!''' + key = request.args.get('api_key') + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + return + user_id = sql.verify_key(key) + if user_id is None: + abort(HTTPStatus.UNAUTHORIZED, "Invalid Key") + isowner = user_id is OWNER_ID + iscowoner = user_id is CO_OWNER_ID + isudo = user_id in SUDO_USERS + isadmin = isowner or iscowoner or isudo + if not isadmin: + abort(HTTPStatus.FORBIDDEN, "User is not bot admin.") + else: + data = request.get_json() + try: + tochange = int(data["user_id"]) + except KeyError: + abort(HTTPStatus.BAD_REQUEST, "Bad Request") + try: + chat_id = str(data["chat_id"]) + except KeyError: + chat_id = None + pass + try: + username = str(data["username"]) + except KeyError: + username = None + pass + + update_user(tochange, username, chat_id) + diff --git a/tg_bot/restapi/resources/modules/__init__.py b/tg_bot/restapi/resources/modules/__init__.py new file mode 100644 index 0000000..1e15628 --- /dev/null +++ b/tg_bot/restapi/resources/modules/__init__.py @@ -0,0 +1,17 @@ +# Nemesis - Powerful Telegram group managment bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/tg_bot/restapi/resources/modules/admin.py b/tg_bot/restapi/resources/modules/admin.py new file mode 100644 index 0000000..f1be8aa --- /dev/null +++ b/tg_bot/restapi/resources/modules/admin.py @@ -0,0 +1,96 @@ +# Nemesis - Powerful Telegram group managment bot +# Copyright (C) 2017 - 2019 Paul Larsen +# Copyright (C) 2019 - 2020 KaratekHD +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from http import HTTPStatus + +from flask import request, abort +from flask_restplus import Namespace, Resource +from telegram import TelegramError + +from tg_bot import dispatcher +from tg_bot.modules.sql import api_sql +import tg_bot.modules.sql.users_sql as user_sql +import tg_bot.modules.sql.mute as mute_sql +import tg_bot.restapi.models.modules.admin as admin_model + +api = Namespace("admin", description="Functions designed to manage basic things inside a group.") + + +@api.route("/") +@api.param('id', 'The group identifier') +@api.response(400, "Bad Request") +@api.response(404, 'Chat not found.') +@api.response(403, "User is not permitted to view this chat.") +@api.response(401, "Unauthorized") +@api.response(410, "Bot is not a member of the chat (anymore).") +class Admins(Resource): + @api.marshal_with(admin_model.create_chat_model(api)) + def get(self, id): + key = request.args.get('api_key') + if not key: + abort(HTTPStatus.UNAUTHORIZED, "Unauthorized") + user_id = api_sql.verify_key(key) + chats = [] + for element in user_sql.get_chats_by_member(user_id): + chats.append(element.chat) + if id not in chats: + abort(HTTPStatus.NOT_FOUND, "Chat not found.") + user_chats = [] + for element in user_sql.get_chats_by_member(user_id): + user_chats.append(element.chat) + if id not in user_chats: + abort(HTTPStatus.FORBIDDEN, "User is not permitted to view this chat.") + is_muted = mute_sql.get_muted(id) + list_groups_str = request.args.get('list_groups') + if list_groups_str: + if list_groups_str == "True" or list_groups_str == "true": + list_groups = True + if list_groups_str == "False" or list_groups_str == "false": + list_groups = False + if list_groups_str.lower() != "false" and list_groups_str.lower() != "true": + abort(HTTPStatus.BAD_REQUEST, "'list_groups' is invalid.") + else: + list_groups = False + try: + groupchat = dispatcher.bot.get_chat(id) + except TelegramError as excp: + if excp.message == 'Chat not found': + abort(HTTPStatus.GONE, "Bot is not a member of the chat (anymore).") + admins = [] + for admin in groupchat.get_administrators(): + data = {} + try: + data.update({"id": admin.user.id}) + chat = admin.user + if chat.username: + data.update({"username": chat.username}) + data.update({"link": chat.link}) + data.update({"first_name": chat.first_name}) + if chat.last_name: + data.update({"last_name": chat.last_name}) + + if list_groups: + groups = [] + for group in user_sql.get_chats_by_member(chat.id): + groups.append([{"id": group.chat, "name": user_sql.get_chatname(group.chat)}]) + data.update({"groups": groups}) + + admins.append(data) + except TelegramError as excp: + if excp.message == 'Chat not found': + pass + return {"id": id, "name" : groupchat.title, "is_muted" : is_muted, "admins" : admins}, HTTPStatus.OK