diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0e5bcf..913f7a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: cd Syncogram flet pack application.py ` --name "Syncogram" ` - --icon "assets\logo\ico\duck128x128.ico" ` + --icon "assets/logo/icns/duck512x512.icns" ` --product-name "Syncogram Application" ` --product-version "${{ env.json_APP_VERSION }}" ` --company-name "Syncogram Application" ` @@ -52,8 +52,8 @@ jobs: --add-binary locales:locales ` --add-data assets:assets ` --add-data config.json:. ` - --distpath ..\craft - cd ..\ + --distpath ../craft + cd ../ - name: Archive Windows Application run: | diff --git a/Syncogram/application.py b/Syncogram/application.py index aa4d632..c345b1c 100644 --- a/Syncogram/application.py +++ b/Syncogram/application.py @@ -37,7 +37,7 @@ async def application(page: ft.Page) -> None: expand=True, ) ) - newest_version(page, cfg["APP"]["VERSION"], _) + newest_version(page, _) if __name__ == "__main__": ft.app(target=application, assets_dir="assets") diff --git a/Syncogram/config.json b/Syncogram/config.json index e1959af..561927b 100644 --- a/Syncogram/config.json +++ b/Syncogram/config.json @@ -1,16 +1,20 @@ { "APP": { "NAME": "Syncogram", - "VERSION": "2024.20.04", + "VERSION": "2024.03.05", "VERSION_IS_ALPHA": "True", "VERSION_IS_BETA": "False" }, + "DATABASE": { + "VERSION": "0.0.1" + }, "GIT": { "REPO_LINK": "https://github.com/pwd491/syncogram.git", "REPO_ISSUES": "https://github.com/pwd491/syncogram/issues", "RELEASES": "https://github.com/pwd491/Syncogram/releases", "BRANCH_MAIN": "master", - "BRANCH_SECOND": "dev" + "BRANCH_SECOND": "dev", + "REMOTE_CONFIG_URL": "https://raw.githubusercontent.com/pwd491/Syncogram/master/Syncogram/config.json" }, "AUTHOR": { "NAME": "Sergey Degtyar", diff --git a/Syncogram/sourcefiles/database/constants.py b/Syncogram/sourcefiles/database/constants.py new file mode 100644 index 0000000..d822feb --- /dev/null +++ b/Syncogram/sourcefiles/database/constants.py @@ -0,0 +1,127 @@ +SQL_TABLE_USERS = \ +""" +CREATE TABLE IF NOT EXISTS users + ( + user_id INTEGER PRIMARY KEY, + is_primary INTEGER, + username VARCHAR(32), + phone VARCHAR(16), + first_name VARCHAR(64), + last_name VARCHAR(64), + restricted INTEGER, + restriction_reason TEXT, + stories_hidden INTEGER, + stories_unavailable INTEGER, + contact_require_premium INTEGER, + scam INTEGER, + fake INTEGER, + premium INTEGER, + photo INTEGER, + emoji_status TEXT, + usernames TEXT, + color TEXT, + profile_color TEXT, + session TEXT, + access_hash INTEGER + ) +""" + +SQL_TABLE_OPTIONS = \ +""" +CREATE TABLE IF NOT EXISTS options +( + user_id INTEGER PREFERENCE UNIQUE, + is_sync_fav INTEGER DEFAULT 0, + is_sync_profile_name INTEGER DEFAULT 0 +) +""" + +SQL_UPDATE_OPTIONS = \ +""" +UPDATE `options` +SET +is_sync_fav = (?), +is_sync_profile_name = (?) +FROM +( + SELECT user_id FROM users WHERE is_primary = 1 +) as users +WHERE +( + options.user_id = users.user_id +) +""" + +SQL_INSERT_OPTIONS = \ +""" +INSERT INTO options +VALUES +( +(SELECT user_id FROM users WHERE is_primary = 1), +(?), +(?) +) +""" + + +SQL_TABLE_DB_DATA = \ +""" +CREATE TABLE IF NOT EXISTS db_data (version VARCHAR) +""" + +SQL_TRIGGER_DEFAULT_OPTIONS = \ +""" +CREATE TRIGGER IF NOT EXISTS defaults_options_on_insert +AFTER INSERT ON users +BEGIN + INSERT INTO options + VALUES + ( + NEW.user_id, + 0, + 0, + 0 + ); + END; +""" + +SQL_TRIGGER_DROP_OPTIONS = \ +""" +CREATE TRIGGER IF NOT EXISTS delete_options_on_delete +BEFORE DELETE + ON users +BEGIN + DELETE FROM options + WHERE + ( + old.user_id = options.user_id + ); +END; +""" + +SQL_ADD_USER = \ +""" +INSERT INTO users +VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +""" + +SQL_GET_USERS = """SELECT * FROM users""" +SQL_GET_USER_ID_BY_STATUS = """SELECT user_id FROM users WHERE is_primary = (?) """ +SQL_GET_USERNAME_BY_STATUS = """SELECT username FROM users WHERE is_primary = (?)""" +SQL_GET_SESSION_BY_ID = """SELECT session FROM users WHERE user_id = ?""" +SQL_GET_SESSION_BY_STATUS = """SELECT session FROM users WHERE is_primary = ?""" +SQL_DELETE_USER_BY_ID = """DELETE FROM users WHERE user_id = ?""" +SQL_DROP_OPTIONS = """DROP TABLE options""" +SQL_GET_DATABASE_VERSION = """SELECT version FROM db_data""" +SQL_INSERT_DB_VERSION = """INSERT INTO db_data VALUES (?)""" +SQL_UPDATE_DB_VERSION = """UPDATE db_data SET version = (?)""" + +SQL_GET_OPTIONS = \ +""" +SELECT * +FROM options +WHERE options.user_id = +( + SELECT user_id FROM users WHERE is_primary = 1 +) +""" diff --git a/Syncogram/sourcefiles/database/consts.py b/Syncogram/sourcefiles/database/consts.py deleted file mode 100644 index d3781f1..0000000 --- a/Syncogram/sourcefiles/database/consts.py +++ /dev/null @@ -1,44 +0,0 @@ -SQL_CREATE_USERS = """ - CREATE TABLE IF NOT EXISTS users ( - user_id INTEGER, - name TEXT NOT NULL, - is_primary BOOLEAN NOT NULL DEFAULT (0), - session TEXT, - PRIMARY KEY (user_id AUTOINCREMENT)) - """ - -SQL_CREATE_OPTIONS = """ - CREATE TABLE IF NOT EXISTS options ( - user_id INTEGER REFERENCES users (user_id) UNIQUE, - is_sync_fav INTEGER DEFAULT 0, - is_sync_pin_fav INTEGER DEFAULT 0, - is_sync_profile_name INTEGER DEFAULT 0, - PRIMARY KEY ( - user_id - ) - ) - """ - -SQL_TRIGGER_DEFAULT_OPTIONS = """ - CREATE TRIGGER IF NOT EXISTS defaults_options_on_insert - AFTER INSERT ON users - BEGIN - INSERT INTO options - VALUES ( - NEW.user_id, - 0, - 0, - 0 - ); - END; - """ - -SQL_TRIGGER_DROP_OPTIONS = """ - CREATE TRIGGER IF NOT EXISTS delete_options_on_delete - BEFORE DELETE - ON users - BEGIN - DELETE FROM options - WHERE (old.user_id = options.user_id); - END; - """ \ No newline at end of file diff --git a/Syncogram/sourcefiles/database/sqlite.py b/Syncogram/sourcefiles/database/sqlite.py index add00ca..ef3f3e3 100644 --- a/Syncogram/sourcefiles/database/sqlite.py +++ b/Syncogram/sourcefiles/database/sqlite.py @@ -1,13 +1,32 @@ -import os, sys +"""Database module of Syncogram application.""" + +import os +import sys import sqlite3 + from contextlib import closing from typing import Any -from .consts import ( - SQL_CREATE_USERS, - SQL_CREATE_OPTIONS, - SQL_TRIGGER_DROP_OPTIONS, - SQL_TRIGGER_DEFAULT_OPTIONS +from ..utils import check_db_version +from .constants import ( + SQL_TABLE_USERS, + SQL_TABLE_OPTIONS, + SQL_TABLE_DB_DATA, + SQL_ADD_USER, + SQL_GET_USERS, + SQL_GET_OPTIONS, + SQL_GET_SESSION_BY_ID, + SQL_GET_SESSION_BY_STATUS, + SQL_GET_USER_ID_BY_STATUS, + SQL_GET_USERNAME_BY_STATUS, + SQL_DELETE_USER_BY_ID, + SQL_INSERT_OPTIONS, + SQL_UPDATE_OPTIONS, + SQL_DROP_OPTIONS, + SQL_GET_DATABASE_VERSION, + SQL_INSERT_DB_VERSION, + SQL_UPDATE_DB_VERSION, + SQL_TRIGGER_DROP_OPTIONS ) WORK_DIR = "" @@ -23,64 +42,132 @@ if not os.path.exists(WORK_DIR) or not os.path.isdir(WORK_DIR): os.mkdir(WORK_DIR) -DB_PATH = os.path.join(WORK_DIR, "test.db") +DB_NAME = "syncogram" +DB_EXTENSION = ".sqlite3" +DB_FILE = os.path.join(WORK_DIR, DB_NAME + DB_EXTENSION) + class SQLite: + """The main class of Database.""" + def __init__(self) -> None: self.database: sqlite3.Connection = sqlite3.connect( - DB_PATH, check_same_thread=False + DB_FILE, check_same_thread=False ) - self.database.cursor().execute(SQL_CREATE_USERS).close() - self.database.cursor().execute(SQL_CREATE_OPTIONS).close() + self.database.cursor().execute(SQL_TABLE_DB_DATA).close() + self.database.cursor().execute(SQL_TABLE_USERS).close() + self.database.cursor().execute(SQL_TABLE_OPTIONS).close() self.database.cursor().execute(SQL_TRIGGER_DROP_OPTIONS).close() - self.database.cursor().execute(SQL_TRIGGER_DEFAULT_OPTIONS).close() + + def add_user(self, *args) -> bool | int: + """Get user data and save to database.""" + with self.database as connect: + with closing(connect.cursor()) as cursor: + try: + request = cursor.execute(SQL_ADD_USER, (*args,)) + return bool(request) + except sqlite3.IntegrityError as error: + return error.sqlite_errorcode def get_users(self) -> list[Any]: + """Get all users.""" with self.database as connect: with closing(connect.cursor()) as cursor: - return cursor.execute("SELECT * FROM users").fetchall() - - def get_user_by_id(self, account_id): + return cursor.execute(SQL_GET_USERS).fetchall() + + def get_session_by_id(self, account_id) -> list[int]: + """Get user by id""" with self.database as connect: with closing(connect.cursor()) as cursor: - return cursor.execute("SELECT session FROM users WHERE user_id = ?", (account_id,)).fetchone() + return cursor.execute( + SQL_GET_SESSION_BY_ID, (account_id,) + ).fetchone()[0] - def get_user_by_status(self, is_primary: int): + def get_user_id_by_status(self, status: int) -> list[str]: + """Get user by id""" with self.database as connect: with closing(connect.cursor()) as cursor: - return cursor.execute("SELECT session FROM users WHERE is_primary = ?", (is_primary,)).fetchone() + return cursor.execute( + SQL_GET_USER_ID_BY_STATUS, (status,) + ).fetchone()[0] - def add_user(self, user_id: int, name: str, is_primary: int, session: str) -> bool: + def get_session_by_status(self, is_primary: int) -> list[str]: + """Get user by status (sender or recepient).""" with self.database as connect: with closing(connect.cursor()) as cursor: - request = cursor.execute( - "INSERT INTO `users` VALUES (?,?,?,?)", - ( - user_id, - name, - is_primary, - session, - ), - ) - return bool(request) - - def delete_user_by_id(self, account_id): + return cursor.execute( + SQL_GET_SESSION_BY_STATUS, (is_primary,) + ).fetchone()[0] + + def get_username_by_status(self, is_primary: int) -> str: + """Get username by status (sender or recepient).""" + with self.database as connect: + with closing(connect.cursor()) as cursor: + return cursor.execute( + SQL_GET_USERNAME_BY_STATUS, (is_primary,) + ).fetchone()[0] + + def delete_user_by_id(self, account_id) -> None: + """Delete user by id.""" with self.database as connect: with closing(connect.cursor()) as cursor: - return cursor.execute("DELETE FROM users WHERE user_id = ?", (account_id,)) + return cursor.execute( + SQL_DELETE_USER_BY_ID, (account_id,) + ) - def get_options(self): + def get_options(self) -> list[int]: + """Get options values only for primary account (sender).""" with self.database as connect: with closing(connect.cursor()) as cursor: return cursor.execute( - "SELECT * FROM options WHERE options.user_id = (SELECT user_id FROM users WHERE is_primary = 1)" + SQL_GET_OPTIONS ).fetchone() def set_options(self, *args) -> bool: + """Set options values only for primary account (sender).""" with self.database as connect: with closing(connect.cursor()) as cursor: - request = cursor.execute( - "UPDATE `options` SET is_sync_fav = (?), is_sync_pin_fav = (?), is_sync_profile_name = (?) FROM (SELECT user_id FROM users WHERE is_primary = 1) as users WHERE (options.user_id = users.user_id)", - (*args,), - ) + user = bool(self.get_options()) + if user: + request = cursor.execute(SQL_UPDATE_OPTIONS, (*args,)) + else: + user = self.get_user_id_by_status(1) + if user: + request = cursor.execute(SQL_INSERT_OPTIONS, (*args,)) + else: + return False return bool(request) + + def get_version(self) -> str | None: + """Get database version from db_data table.""" + with self.database as connect: + with closing(connect.cursor()) as cursor: + request = cursor.execute(SQL_GET_DATABASE_VERSION).fetchone() + if bool(request): + return request[0] + return None + + def set_version(self, version) -> None: + """Set database version if not exists.""" + with self.database as connect: + with closing(connect.cursor()) as cursor: + cursor.execute(SQL_INSERT_DB_VERSION, (version,)) + + def update_version(self, version) -> None: + """Update database version if exists newest.""" + with self.database as connect: + with closing(connect.cursor()) as cursor: + cursor.execute(SQL_UPDATE_DB_VERSION, (version,)) + + def check_update(self): + """Check and update database options.""" + current_db_version = self.get_version() + if current_db_version is not None: + need_to_update = check_db_version(current_db_version) + if isinstance(need_to_update, tuple): + self.database.cursor().execute(SQL_DROP_OPTIONS).close() + self.database.cursor().execute(SQL_TABLE_OPTIONS).close() + self.update_version(need_to_update[1]) + else: + from ..utils import get_local_database_version + self.set_version(get_local_database_version()) diff --git a/Syncogram/sourcefiles/telegram/client.py b/Syncogram/sourcefiles/telegram/client.py index 4480e54..3ef0550 100644 --- a/Syncogram/sourcefiles/telegram/client.py +++ b/Syncogram/sourcefiles/telegram/client.py @@ -5,11 +5,19 @@ from telethon.sessions import StringSession from telethon.tl.custom.qrlogin import QRLogin from telethon.tl.types import InputPeerUser, User -from telethon.errors import SessionPasswordNeededError, PasswordHashInvalidError +from telethon.errors import ( + SessionPasswordNeededError, + PasswordHashInvalidError, + UsernameNotModifiedError, + UsernameInvalidError, + UsernameOccupiedError + ) +from telethon import functions from ..database import SQLite from ..utils import config from ..utils import generate_qrcode +from ..utils import generate_username from .environments import API_ID, API_HASH cfg = config() @@ -54,13 +62,47 @@ async def login_by_qrcode(self, dialog, is_primary): dialog.open = False dialog.update() user: User | InputPeerUser = await self.get_me() - self.database.add_user( - user.id, # type: ignore - user.first_name, # type: ignore + if user.username is None: + while True: + try: + username = generate_username() + await self(functions.account.UpdateUsernameRequest( + username + )) + user.username = username + break + except ( + UsernameNotModifiedError, + UsernameInvalidError, + UsernameOccupiedError + ): + continue + + response = self.database.add_user( + user.id, is_primary, - self.session.save(), # type: ignore + user.username, + user.phone, + user.first_name, + user.last_name, + int(user.restricted), + str(user.restriction_reason), + int(user.stories_hidden), + int(user.stories_unavailable), + int(user.contact_require_premium), + int(user.scam), + int(user.fake), + int(user.premium), + user.photo.photo_id if user.photo is not None else None, + str(user.emoji_status), + str(user.usernames), + user.color, + str(user.profile_color), + self.session.save(), + user.access_hash ) self.disconnect() + return response async def logout(self): if not self.is_connected(): diff --git a/Syncogram/sourcefiles/telegram/manager.py b/Syncogram/sourcefiles/telegram/manager.py index 327de9f..91f2719 100644 --- a/Syncogram/sourcefiles/telegram/manager.py +++ b/Syncogram/sourcefiles/telegram/manager.py @@ -1,46 +1,48 @@ -from asyncio import sleep +import asyncio import flet as ft from telethon.tl.functions.account import UpdateProfileRequest from telethon.tl.functions.users import GetFullUserRequest -from telethon.tl.types import UserFull +from telethon.tl.patched import MessageService +from telethon.tl.types import UserFull, PeerUser, Message from .client import UserClient from .task import CustomTask from ..database import SQLite from ..userbar.settings import SettingsDialog + class Manager: - def __init__(self, page: ft.Page, _, mainwindow = None) -> None: + def __init__(self, page: ft.Page, _, mainwindow=None) -> None: self.page: ft.Page = page self.database = SQLite() self.mainwindow = mainwindow self.client = UserClient - + self.options = { "is_sync_fav": { "title": _("Sync my favorite messages between accounts."), "function": self.sync_favorite_messages, "status": bool(), - "ui_task_object": CustomTask - }, - "is_sync_pin_fav": { - "title": _("Synchronize the sequence of pinned messages in your favorite messages."), - "function": self.sync_sequence_of_pinned_messages, - "status": bool(), - "ui_task_object": CustomTask + "ui_task_object": CustomTask, }, "is_sync_profile_name": { - "title": _("Synchronize the first name, last name and biography of the profile."), + "title": _( + "Synchronize the first name, last name and biography of the profile." + ), "function": self.sync_profile_first_name_and_second_name, "status": bool(), - "ui_task_object": CustomTask - } + "ui_task_object": CustomTask, + }, } async def build(self): - list_of_options = self.database.get_options()[1:] - + + list_of_options = self.database.get_options() + if list_of_options is None: + return + list_of_options = list_of_options[1:] + for n, option in enumerate(self.options.items()): option[1].update({"status": bool(list_of_options[n])}) @@ -48,63 +50,170 @@ async def build(self): if option[1].get("status"): title = option[1].get("title") task = CustomTask(title) - option[1].update( - { - "ui_task_object": task - } - ) + option[1].update({"ui_task_object": task}) self.mainwindow.wrapper_side_column.controls.append(task) self.mainwindow.update() - async def sync_favorite_messages(self, ui_task_object: CustomTask): - """ - Важно учитывать: - a. Последовательность отвеченных сообщений - b. Контекст сообщения, для навигации используют хештег - c. Статус о закреплении сообщения в диалоге - - Важно итерировать диалог, таким образом получится достичь максимума - информации для каждого сообщения. Стоит коолекционировать данные, для - следующих функций, где мы могли бы переиспользовать эти данные. - """ - pass + # ui_task_object.progress.value = None + # ui_task_object.progress.update() + + sender = self.client(self.database.get_session_by_status(1)) + recepient = self.client(self.database.get_session_by_status(0)) + + sender_username = self.database.get_username_by_status(1) + recepient_username = self.database.get_username_by_status(0) + + if not (sender.is_connected() and recepient.is_connected()): + await sender.connect() + await recepient.connect() + + sender_entity = await recepient.get_input_entity(sender_username) + recepient_entity = await sender.get_input_entity(recepient_username) - async def sync_sequence_of_pinned_messages(self, ui_task_object: CustomTask): - await sleep(3) - ui_task_object.progress.value = 1 - ui_task_object.header.controls.pop(-1) - ui_task_object.header.controls.append(ft.Icon(ft.icons.TASK_ALT, color=ft.colors.GREEN)) - ui_task_object.border = ft.Border = ft.border.all(0.5, ft.colors.GREEN) - ui_task_object.update() + # Getting messages from sender source + source_messages = sender.iter_messages( + sender_entity, min_id=0, max_id=0, reverse=True + ) + is_grouped_id = None + is_pinned = False + is_replied = False + group = [] + msg_ids = {} + + async def recepient_save_message( + message_id, message_length, is_pin: bool, is_reply: None | Message + ): + data = await recepient.get_messages(sender_entity, limit=message_length) + messages = [message for message in data] + if is_reply: + destination_message_id = msg_ids.get(is_reply.reply_to_msg_id) + if messages[0].media: + await asyncio.sleep(3) + message = await recepient.send_message( + recepient_entity, + messages[-1].message, + file=messages, + reply_to=destination_message_id, + ) + msg_ids[message_id] = message[0].id + else: + await asyncio.sleep(3) + message = await recepient.send_message( + recepient_entity, + message=messages[0], + reply_to=destination_message_id, + ) + msg_ids[message_id] = message.id + else: + message = await recepient.forward_messages(recepient_entity, messages) + msg_ids[message_id] = message[0].id + + if is_pin: + await asyncio.sleep(3) + await recepient.pin_message(recepient_entity, message[0]) + await asyncio.sleep(3) + await recepient.delete_messages(sender_entity, messages) + + try: + ui_task_object.progress_counters.visible = True + i = 0 + async for message in source_messages: + i += 1 + ui_task_object.total.value = source_messages.total + ui_task_object.progress.value = i / source_messages.total + ui_task_object.value.value = i + ui_task_object.update() + # await asyncio.sleep(0.00001) + if not isinstance(message, MessageService): + if message.grouped_id is not None: + if message.pinned: + is_pinned = True + if message.reply_to: + is_replied = message.reply_to + if is_grouped_id != message.grouped_id: + is_grouped_id = message.grouped_id + if group: + await asyncio.sleep(3) + await sender.forward_messages( + recepient_entity, group, silent=True + ) + await recepient_save_message( + message.id, len(group), is_pinned, is_replied + ) + is_pinned = False + is_replied = False + group.clear() + group.append(message) + continue + if group: + await asyncio.sleep(3) + await sender.forward_messages( + recepient_entity, group, silent=True + ) + await recepient_save_message( + message.id, len(group), is_pinned, is_replied + ) + is_pinned = False + is_replied = False + group.clear() + + await asyncio.sleep(3) + await sender.forward_messages( + recepient_entity, message, silent=True + ) + await recepient_save_message( + message.id, 1, message.pinned, message.reply_to + ) + + if group: + await asyncio.sleep(3) + await sender.forward_messages(recepient_entity, group, silent=True) + await recepient_save_message( + group[-1].id, len(group), is_pinned, is_replied + ) + is_pinned = False + is_replied = False + group.clear() + except Exception as e: + return ui_task_object.unsuccess(e) + finally: + del msg_ids + sender.disconnect() + recepient.disconnect() + + ui_task_object.success() async def sync_profile_first_name_and_second_name(self, ui_task_object: CustomTask): + """ + Connecting accounts, getting profile data and sets. + """ ui_task_object.progress.value = None ui_task_object.progress.update() - self.sender = self.client(*self.database.get_user_by_status(1)) - self.recepient = self.client(*self.database.get_user_by_status(0)) + sender = self.client(self.database.get_session_by_status(1)) + recepient = self.client(self.database.get_session_by_status(0)) - if not (self.sender.is_connected() and self.recepient.is_connected()): - await self.sender.connect() - await self.recepient.connect() + if not (sender.is_connected() and recepient.is_connected()): + await sender.connect() + await recepient.connect() try: - user: UserFull = await self.sender(GetFullUserRequest("me")) + user: UserFull = await sender(GetFullUserRequest("me")) first_name = user.users[0].first_name first_name = "" if first_name is None else first_name last_name = user.users[0].last_name last_name = "" if last_name is None else last_name bio = user.full_user.about bio = "" if bio is None else bio - - await self.recepient(UpdateProfileRequest(first_name, last_name, bio)) + + await recepient(UpdateProfileRequest(first_name, last_name, bio)) except Exception as e: return ui_task_object.unsuccess(e) - self.sender.disconnect() - self.recepient.disconnect() + sender.disconnect() + recepient.disconnect() ui_task_object.success() async def start_all_tasks(self, btn, _): @@ -114,7 +223,7 @@ async def start_all_tasks(self, btn, _): settings.open = True btn.state = False return self.page.update() - + for option in self.options.items(): if option[1].get("status"): func = option[1].get("function") diff --git a/Syncogram/sourcefiles/telegram/task.py b/Syncogram/sourcefiles/telegram/task.py index d95786e..fcba172 100644 --- a/Syncogram/sourcefiles/telegram/task.py +++ b/Syncogram/sourcefiles/telegram/task.py @@ -12,6 +12,16 @@ def __init__(self, title: str) -> None: self.progress = ft.ProgressBar() self.progress.value = 0 + self.value = ft.Text() + self.value.value = 0 + self.total = ft.Text() + self.total.value = 0 + + self.progress_counters = ft.Row() + self.progress_counters.controls = [self.value, self.total] + self.progress_counters.alignment = ft.MainAxisAlignment.SPACE_BETWEEN + self.progress_counters.visible = False + self.header = ft.Row() self.header.controls = [self.title, ft.Icon(ft.icons.UPDATE, color=ft.colors.ORANGE_500)] self.header.alignment = ft.MainAxisAlignment.SPACE_BETWEEN @@ -20,6 +30,7 @@ def __init__(self, title: str) -> None: self.wrapper = ft.Column([ self.header, ft.Divider(opacity=0), + self.progress_counters, self.progress ]) diff --git a/Syncogram/sourcefiles/userbar/authenticate.py b/Syncogram/sourcefiles/userbar/authenticate.py index 61e7ec9..a5e314e 100644 --- a/Syncogram/sourcefiles/userbar/authenticate.py +++ b/Syncogram/sourcefiles/userbar/authenticate.py @@ -13,12 +13,26 @@ def __init__(self, page: ft.Page, _, *args, **kwargs) -> None: self.is_primary = args[1] self.update_mainwindow = args[2] self.password_inputed_event = Event() + self.client = UserClient() + self.qrcode_image = ft.Image("1") - self.log_phone_number = ft.TextButton() - self.log_phone_number.text = _("Use phone number") - self.log_phone_number.disabled = True + self.log_phone_number_button = ft.TextButton() + self.log_phone_number_button.text = _("Use phone number") + self.log_phone_number_button.on_click = self.phone_login_dialog + self.log_phone_number_button.disabled = True + + self.log_qrcode_button = ft.TextButton() + self.log_qrcode_button.text = _("Use QR-code") + self.log_qrcode_button.on_click = ... + self.log_qrcode_button.visible = False + + + self.phone_field = ft.TextField() + self.phone_field.keyboard_type = ft.KeyboardType.PHONE + self.phone_field.visible = False + self.password = ft.TextField() self.password.label = _("2FA password") @@ -31,7 +45,8 @@ def __init__(self, page: ft.Page, _, *args, **kwargs) -> None: self.modal = True self.content = ft.Column( - [ + [ + self.phone_field, self.qrcode_image, self.password ] @@ -41,7 +56,7 @@ def __init__(self, page: ft.Page, _, *args, **kwargs) -> None: self.content.alignment = ft.MainAxisAlignment.CENTER self.content.horizontal_alignment = ft.CrossAxisAlignment.CENTER self.title = ft.Text(_("Authorization")) - self.actions = [self.button_close, self.log_phone_number] + self.actions = [self.button_close, self.log_phone_number_button, self.log_qrcode_button] self.actions_alignment = ft.MainAxisAlignment.SPACE_BETWEEN async def close(self, e): @@ -56,19 +71,28 @@ async def submit(self, e): async def input_2fa_password(self): self.actions.append(self.button_submit) - self.log_phone_number.visible = False + self.log_phone_number_button.visible = False + # self.log_qrcode_button.visible = True self.qrcode_image.visible = False self.password.visible = True self.update() async def qr_login_dialog(self): - client = UserClient() - await client.login_by_qrcode(dialog=self, is_primary=self.is_primary) + await self.client.login_by_qrcode(dialog=self, is_primary=self.is_primary) + # Need to except 1555 error UNIQUE ID PRIMARY KEY (user exists.) await self.update_mainwindow() await self.update_accounts.generate() await self.update_accounts.update_async() self.update() + async def phone_login_dialog(self, e): + self.client.disconnect() + self.qrcode_image.visible = False + self.log_qrcode_button.visible = True + self.log_phone_number_button.visible = False + self.phone_field.visible = True + self.update() + async def error(self): self.password.border_color = ft.colors.RED self.password.focus() diff --git a/Syncogram/sourcefiles/userbar/generate.py b/Syncogram/sourcefiles/userbar/generate.py index 49d7dc4..e0e056b 100644 --- a/Syncogram/sourcefiles/userbar/generate.py +++ b/Syncogram/sourcefiles/userbar/generate.py @@ -9,8 +9,9 @@ from ..database import SQLite -class UIGenerateAccounts(ft.UserControl): +class UIGenerateAccounts(ft.Container): def __init__(self, page: ft.Page, _, *args, **kwargs) -> None: + super().__init__() self.page: ft.Page = page self.database = SQLite() self._ = _ @@ -38,10 +39,10 @@ def __init__(self, page: ft.Page, _, *args, **kwargs) -> None: self.wrapper_column = ft.Column() self.wrapper_column.controls = [self.account_primary, self.account_secondary] - self.wrapper = ft.Container() - self.wrapper.content = self.wrapper_column + # self.wrapper = ft.Container() + # self.wrapper.content = self.wrapper_column - super().__init__() + self.content = self.wrapper_column def account_button(self, account_id, account_name) -> ft.ElevatedButton: button = ft.ElevatedButton() @@ -101,15 +102,15 @@ async def generate(self) -> None: while len(self.account_secondary.controls) > 3: self.account_secondary.controls.pop(-2) for account in accounts: - if bool(account[2]): + if bool(account[1]): self.account_primary.controls.insert( - -1, self.account_button(account[0], account[1]) + -1, self.account_button(account[0], account[4][0:16]) ) else: self.account_secondary.controls.insert( - -1, self.account_button(account[0], account[1]) + -1, self.account_button(account[0], account[4][0:16]) ) self.update() def build(self) -> ft.Container: - return self.wrapper + return self diff --git a/Syncogram/sourcefiles/userbar/logout.py b/Syncogram/sourcefiles/userbar/logout.py index b46dcf9..b8c38fa 100644 --- a/Syncogram/sourcefiles/userbar/logout.py +++ b/Syncogram/sourcefiles/userbar/logout.py @@ -22,7 +22,7 @@ def __init__(self, account_id, _, *args) -> None: self.actions_alignment = ft.MainAxisAlignment.CENTER async def submit(self, e) -> None: - session = self.database.get_user_by_id(self.account_id) + session = self.database.get_session_by_id(self.account_id) client = UserClient(*session) if await client.logout(): self.database.delete_user_by_id(self.account_id) diff --git a/Syncogram/sourcefiles/userbar/main.py b/Syncogram/sourcefiles/userbar/main.py index a0d1104..1289850 100644 --- a/Syncogram/sourcefiles/userbar/main.py +++ b/Syncogram/sourcefiles/userbar/main.py @@ -22,7 +22,6 @@ def __init__(self, page: ft.Page, update_mainwin, _) -> None: self.settings_btn.text = _("Settings") self.settings_btn.icon = ft.icons.SETTINGS self.settings_btn.expand = True - # self.settings_btn.width = 200 self.settings_btn.height = 45 self.settings_btn.on_click = self.settings @@ -64,7 +63,7 @@ async def settings(self, e) -> None: settings = SettingsDialog(self.update_mainwin, self._) self.page.dialog = settings settings.open = True - await self.page.update_async() + self.page.update() async def generate_accounts_callback(self): await self.generate_accounts.generate() diff --git a/Syncogram/sourcefiles/userbar/settings.py b/Syncogram/sourcefiles/userbar/settings.py index c63bc53..64e2297 100644 --- a/Syncogram/sourcefiles/userbar/settings.py +++ b/Syncogram/sourcefiles/userbar/settings.py @@ -15,18 +15,12 @@ def __init__(self, update_mainwin, _) -> None: self.c1 = ft.Checkbox( label=_("Sync my favorite messages"), value=bool(self.options[0]), - disabled=True, + disabled=False, tooltip=_("It will be available in the next updates") ) self.c2 = ft.Checkbox( - label=_("Save the sequence of pinned messages"), - value=bool(self.options[1]), - disabled=True, - tooltip=_("It will be available in the next updates") - ) - self.c3 = ft.Checkbox( label=_("Sync my profile first name, last name and biography."), - value=bool(self.options[2]), + value=bool(self.options[1]), disabled=False ) """!!!""" @@ -34,7 +28,6 @@ def __init__(self, update_mainwin, _) -> None: x = [ self.c1, self.c2, - self.c3, ] x.sort(key=lambda x: x.disabled == True) @@ -63,7 +56,6 @@ async def save(self, e) -> None: self.database.set_options( int(self.c1.value), # type: ignore int(self.c2.value), # type: ignore - int(self.c3.value) # type: ignore ) await self.update_mainwin() self.open = False diff --git a/Syncogram/sourcefiles/utils.py b/Syncogram/sourcefiles/utils.py index 13e4c75..07e8567 100644 --- a/Syncogram/sourcefiles/utils.py +++ b/Syncogram/sourcefiles/utils.py @@ -1,6 +1,8 @@ import os import base64 import json +import random +import string from json import loads from requests import request from io import BytesIO @@ -9,16 +11,6 @@ import flet as ft import qrcode - -def config(): - dir = os.path.dirname(os.path.dirname(__file__)) - cfg = os.path.join(dir, "config.json") - - if os.path.isfile(cfg): - with open(cfg, "r", encoding="utf-8") as cfg: - return json.load(cfg) - - def generate_qrcode(url): buffered = BytesIO() QRcode = qrcode.QRCode( @@ -31,14 +23,66 @@ def generate_qrcode(url): img.save(buffered, format="PNG") return base64.b64encode(buffered.getvalue()).decode("utf-8") -def newest_version(page: ft.Page, __version__, _) -> None: - __newest__ = loads( + +def generate_username(): + """Generate random username.""" + letters = string.ascii_letters + string.digits + return \ + ''.join(random.choice(letters) for _ in range(random.randint(5, 32))) + + +if __name__ == '__main__': + generate_username() + +def config(): + dir = os.path.dirname(os.path.dirname(__file__)) + cfg = os.path.join(dir, "config.json") + + if os.path.isfile(cfg): + with open(cfg, "r", encoding="utf-8") as cfg: + return json.load(cfg) + +def get_remote_application_version(): + return \ + loads( + request( + "GET", + config()["GIT"]["REMOTE_CONFIG_URL"], + timeout=15 + ).text + )["APP"]["VERSION"] + +def get_local_appication_version(): + return config()["APP"]["VERSION"] + +def get_local_database_version(): + return config()["DATABASE"]["VERSION"] + +def get_remote_database_version(): + return \ + loads( request( "GET", - "https://raw.githubusercontent.com/pwd491/Syncogram/dev/Syncogram/config.json", + config()["GIT"]["REMOTE_CONFIG_URL"], timeout=15 - ).text - )["APP"]["VERSION"] + ).text + )["DATABASE"]["VERSION"] + + +def check_db_version(__version__) -> tuple | None: + remote_app_version = get_remote_application_version() + local_app_version = get_local_appication_version() + remote_db_version = get_remote_database_version() + local_db_version = __version__ + + if local_db_version != remote_db_version and \ + local_app_version == remote_app_version: + return (True, remote_db_version) + + +def newest_version(page: ft.Page, _) -> None: + __version__ = get_local_appication_version() + __newest__ = get_remote_application_version() if __version__ != __newest__: icon = ft.Icon() icon.name = ft.icons.BROWSER_UPDATED @@ -60,7 +104,4 @@ def newest_version(page: ft.Page, __version__, _) -> None: snack.bgcolor = ft.colors.BLACK87 page.snack_bar = snack page.snack_bar.open = True - page.update() - - - + page.update() \ No newline at end of file diff --git a/Syncogram/sourcefiles/window/main.py b/Syncogram/sourcefiles/window/main.py index 8cf34fe..fb51172 100644 --- a/Syncogram/sourcefiles/window/main.py +++ b/Syncogram/sourcefiles/window/main.py @@ -58,6 +58,7 @@ def __init__(self, page: ft.Page, _) -> None: super().__init__() self.page = page self.database = SQLite() + self.database.check_update() self.manager = Manager(self.page, _, self) self.button_start = ButtonStartTasks(self.manager, _) @@ -103,7 +104,7 @@ def __init__(self, page: ft.Page, _) -> None: self.border_radius = ft.BorderRadius(10, 10, 10, 10) self.padding = 20 - + async def callback_update(self) -> None: users = self.database.get_users() self.wrapper.controls.clear() diff --git a/Syncogram/test.py b/Syncogram/test.py deleted file mode 100644 index 2198e2a..0000000 --- a/Syncogram/test.py +++ /dev/null @@ -1,3 +0,0 @@ -from sourcefiles.utils import config - -print(config()["APP"]["VERSION"]) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..4700bf2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,28 @@ +### Важно: +- [x] Если локальный конфигурационный файл содержит в себе версию приложения совпадающую с файлом из удалённого репозитория **AND** локальный файл содержит в себе не совпадающую версию базы данных из удалённого репозитория, то только в этом случае нужно удалить таблицу опций пользователя и продолжить выполнение кода (пересоздать таблицу опций с новыми параметрами). +- [ ] Если **имя пользователя** не установлено, нужно предложить установить случайный или попросить самому установить в официальном приложении. Перед тем как начать выполнять задачи, нужно проверить на двустороннее существование имён пользователей для корректного нахождения сущностей аккаунтов. + - [x] Скрыто устанавливать имя пользователя (временное решение). +- [x] В **release.py** добавить увелечение версии базы данных, если были произведены изменения функционала. +- [x] Правильная индикация прогресса при выполнении задачи. +- [ ] Переписать синхронизацию избранных сообщений включая следующие механизмы: + - [ ] Пересылать за раз 100 сообщений (максимальное кол-во допустимое тг) + - [ ] Сохранять последовательность + - [ ] Сохранять последовательность отвеченных сообщений в этом же чате. + - [ ] Подумать над коллизеей сообщений, убедиться что группированные сообщения входят допустимое значение =< 100. Максимальное количество группированных сообщений = 10, значит надо тут ещё подумать... + + +### Второстепенно: +- [ ] Найти решение отказаться от использования нахождения сущностей строго по **username**. Механизмы телеграмма не позволяют писать человеку по его **ID**, если **access_hash** не содержит информацию об этом чате. То есть, если общение между двумя собеседниками не было и чат между ними не существует, то отправить сообщение указывая ID аккаунта не получится. +- [ ] При попытке авторизовать уже существующий аккаунт в программе, это нужно обработать. SQL алгоритм уже готов, нужно допилить только GUI. +- [ ] Добавить авторизацию по номеру телефона, болван! +- [ ] Отображать пользовательский аватар вместо иконки. +- [ ] Проверять актуальность данных пользователя при запуске программы (если пользователь авторизовался в программе). + + +### Будущие функции: +- [x] Синхронизация имени, фамилии и описания. +- [x] Синхронизация избранных сообщений. **50/50** +- [ ] Синхронизация пользовательских фотографий. +- [ ] Синхронизация настроек конфиденциальности. +- [ ] Синхронизация публичных каналов и групп. +- [ ] **HARD** Синхронизация закрытых каналов и групп (с автоматическим поиском/парсером ссылок приглашения). diff --git a/release.py b/release.py index 40f76fd..6974383 100644 --- a/release.py +++ b/release.py @@ -2,6 +2,7 @@ import sys import json import time +import datetime yes = {"yes", "y", "ye", ""} no = {"no", "n"} @@ -11,12 +12,26 @@ with open("Syncogram/config.json", "r", encoding="utf-8") as f: data = json.load(f) - CURRENT_VERSION = data["APP"]["VERSION"] + CURRENT_APP_VERSION = data["APP"]["VERSION"] + CURRENT_DB_VERSION = data["DATABASE"]["VERSION"] -NEW_VERSION = str(input(f"Specify the new version ({CURRENT_VERSION}): ")) +now = datetime.datetime.strftime(datetime.datetime.now(), "%Y.%d.%m") +NEW_APP_VERSION = str(input(f"What is new application version ({CURRENT_APP_VERSION}) -> ({now}): ")) + +if NEW_APP_VERSION == "": + NEW_APP_VERSION = now print("---------------------") -print(f"New version: {NEW_VERSION}") -data["APP"]["VERSION"] = NEW_VERSION +print(f"New application version: {NEW_APP_VERSION}") + +NEW_DB_VERSION = str(input(f"What is new Database version ({CURRENT_DB_VERSION}): ")) +if NEW_DB_VERSION == "": + NEW_DB_VERSION = CURRENT_DB_VERSION + print(f"Stay on: {NEW_DB_VERSION}") +else: + print(f"New database version: {NEW_DB_VERSION}") + +data["DATABASE"]["VERSION"] = NEW_DB_VERSION +data["APP"]["VERSION"] = NEW_APP_VERSION print("---------------------") choice = input("Start merge? yes/no: ").lower() @@ -26,13 +41,13 @@ def merge(): json.dump(data, f, ensure_ascii=False, indent=4) os.system("git add .") time.sleep(1) - os.system(f"""git commit -am "Version {NEW_VERSION}" """) + os.system(f"""git commit -am "Version {NEW_APP_VERSION}" """) time.sleep(1) os.system("git push") time.sleep(3) os.system("git checkout master") os.system("git add .") - os.system(f"""git commit -am "Version {NEW_VERSION}" """) + os.system(f"""git commit -am "Version {NEW_APP_VERSION}" """) os.system("git push") time.sleep(3) os.system("git merge dev")