From a534f67c49db728c6bce2ff597745dc43069a5f7 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 20 Nov 2020 13:26:54 +0100 Subject: [PATCH 01/51] Fix #4 and #5 --- mastochess.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mastochess.py b/mastochess.py index d352f86..50c9ebe 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1131,13 +1131,19 @@ if __name__ == '__main__': print(v_error) + toot_text = "@"+username + ' moviment il·legal! (' + moving + '!?)\n' + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + pass except AssertionError as a_error: print(a_error) - toot_text = "@"+username + ' moviment il·legal!' + '\n' + toot_text = "@"+username + ' moviment il·legal! (' + moving + '!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) -- 2.34.1 From a6ca40b4b7a11ddd20c6433196f7642d50fef508 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 20 Nov 2020 15:06:14 +0100 Subject: [PATCH 02/51] Solve #5 issue --- mastochess.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/mastochess.py b/mastochess.py index 50c9ebe..2ebbdfa 100644 --- a/mastochess.py +++ b/mastochess.py @@ -239,6 +239,8 @@ def current_games(): game_link_lst = [] + next_move_lst = [] + try: conn = None @@ -247,7 +249,7 @@ def current_games(): cur = conn.cursor() - cur.execute("select white_user, black_user, chess_status, chess_link from games where not finished") + cur.execute("select white_user, black_user, chess_status, chess_link, next_move from games where not finished") rows = cur.fetchall() @@ -267,9 +269,11 @@ def current_games(): game_link_lst.append(row[3]) + next_move_lst.append(row[4]) + cur.close() - return (player1_name_lst, player2_name_lst, game_status_lst, game_link_lst) + return (player1_name_lst, player2_name_lst, game_status_lst, game_link_lst, next_move_lst) except (Exception, psycopg2.DatabaseError) as error: @@ -1199,7 +1203,7 @@ if __name__ == '__main__': elif query_word == 'jocs': - player1_name_lst, player2_name_lst, game_status_lst, game_link_lst = current_games() + player1_name_lst, player2_name_lst, game_status_lst, game_link_lst, next_move_lst = current_games() if len(player1_name_lst) > 0: @@ -1214,7 +1218,13 @@ if __name__ == '__main__': else: - toot_text += "\n" + player1_name_lst[i] + " / " + player2_name_lst[i] + " (en joc)" + "\n" + if next_move_lst[i] == player1_name_lst[i]: + + toot_text += "\n*" + player1_name_lst[i] + " / " + player2_name_lst[i] + " (en joc)" + "\n" + + else: + + toot_text += "\n" + player1_name_lst[i] + " / *" + player2_name_lst[i] + " (en joc)" + "\n" if game_link_lst[i] != None: -- 2.34.1 From ca97aa187d61f3e14cf2c8d9cd1a7a4c8345f92b Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 20 Nov 2020 17:58:53 +0100 Subject: [PATCH 03/51] Removed unneeded function get_status_url and added moves to victory post --- mastochess.py | 54 +++++++-------------------------------------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/mastochess.py b/mastochess.py index 2ebbdfa..72ac1e1 100644 --- a/mastochess.py +++ b/mastochess.py @@ -381,46 +381,6 @@ def check_games(): conn.close() -def get_status_url(toot_id): - - toot_id = str(toot_id.id) - - try: - - conn = None - - conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") - - cur = conn.cursor() - - select_query = "select uri from statuses where id=(%s)" - - cur.execute(select_query, (toot_id,)) - - row = cur.fetchone() - - if row != None: - - toot_url = row[0] - - else: - - toot_url = '' - - cur.close() - - return toot_url - - except (Exception, psycopg2.DatabaseError) as error: - - sys.exit(error) - - finally: - - if conn is not None: - - conn.close() - def new_game(toot_url): try: @@ -953,7 +913,7 @@ if __name__ == '__main__': toot_id = mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) - toot_url = get_status_url(toot_id) + toot_url = toot_id.uri new_game(toot_url) @@ -979,7 +939,7 @@ if __name__ == '__main__': toot_text += '\n' - toot_text += "@"+white_user + ": et toca a tu" + "\n" + toot_text += "@"+white_user + ": el teu torn" + "\n" toot_text += '\n' @@ -1055,7 +1015,7 @@ if __name__ == '__main__': if board.is_check() == True: - toot_text = "@"+username + " t'ha fet escac!" + toot_text = "@"+playing_user + ": " + "@"+username + " t'ha fet escac!" if username == white_user: @@ -1069,7 +1029,9 @@ if __name__ == '__main__': if board.is_game_over() == True: - toot_text += "\nEscac i mat! \nEl guanyador és: " + "@"+username + '\n' + game_moves = board.ply() + + toot_text += "\nEscac i mat! (en " + str(game_moves) + " moviments)" + "\n\nEl guanyador és: " + "@"+username + '\n' toot_text += "\n@"+playing_user + ": ben jugat!" + "\n" @@ -1119,7 +1081,7 @@ if __name__ == '__main__': toot_id = mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) - toot_url = get_status_url(toot_id) + toot_url = toot_id.uri board_game = board.fen() @@ -1234,8 +1196,6 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) - else: toot_text = "@"+username + " cap partida en joc" + "\n" -- 2.34.1 From 4053a73ad4350c0e98eb17beffcf11f4601643f3 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 20 Nov 2020 18:33:29 +0100 Subject: [PATCH 04/51] Fix #6 --- mastochess.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mastochess.py b/mastochess.py index 72ac1e1..29ebc51 100644 --- a/mastochess.py +++ b/mastochess.py @@ -997,9 +997,9 @@ if __name__ == '__main__': try: - if chess.Move.from_uci(moving) in board.legal_moves == False: + if chess.Move.from_uci(moving) not in board.legal_moves: - toot_text = "@"+username + ' moviment il·legal!' + '\n' + toot_text = "@"+username + ": " + moving + " és un moviment il·legal. Torna a tirar." + "\n" mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) -- 2.34.1 From 8c60d051efc393a9d7bdc92bc89bedaac73c2497 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 21 Nov 2020 19:58:30 +0100 Subject: [PATCH 05/51] Added a warning to player in turn when has been captured one of its pieces --- README.md | 1 + mastochess.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/README.md b/README.md index f1ba3f2..c5b0374 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,4 @@ Within Python Virtual Environment: ![board](board.png) 20.11.2020 - New feature! added link to on going games in games list +21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces diff --git a/mastochess.py b/mastochess.py index 29ebc51..a969b20 100644 --- a/mastochess.py +++ b/mastochess.py @@ -89,6 +89,34 @@ def get_user_domain(account_id): conn.close() +def get_piece_name(captured_piece): + + if captured_piece == 1: + + piece_name = "un Peó" + + if captured_piece == 2: + + piece_name = "un cavall" + + if captured_piece == 3: + + piece_name = "l'àlfil" + + if captured_piece == 4: + + piece_name = "una torre" + + if captured_piece == 5: + + piece_name = "la Reina" + + if captured_piece == 6: + + piece_name = "el Rei" + + return piece_name + def get_notification_data(): try: @@ -1011,6 +1039,20 @@ if __name__ == '__main__': playing_user = next_move(username) + if bool(board.is_capture(chess.Move.from_uci(moving))): + + capture = True + + square_capture_index = chess.SQUARE_NAMES.index(moving[2:]) + + captured_piece = board.piece_type_at(square_capture_index) + + piece_name = get_piece_name(captured_piece) + + else: + + capture = False + board.push(chess.Move.from_uci(moving)) if board.is_check() == True: @@ -1051,6 +1093,10 @@ if __name__ == '__main__': toot_text = "@"+playing_user + ' el teu torn.'+ '\n' + if capture: + + toot_text += "\n* has perdut " + piece_name + "!\n" + toot_text += '\n#escacs' + '\n' if username == white_user: -- 2.34.1 From 3253f95d5f8bec432469aa91c2908d73fe08c57b Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 21 Nov 2020 19:59:29 +0100 Subject: [PATCH 06/51] Added a warning to player in turn when has been captured one of its pieces --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5b0374..8002757 100644 --- a/README.md +++ b/README.md @@ -41,5 +41,5 @@ Within Python Virtual Environment: ![board](board.png) -20.11.2020 - New feature! added link to on going games in games list +20.11.2020 - New feature! added link to on going games in games list 21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces -- 2.34.1 From c7b17abe181db8fe417ae442fe6b2f0e8df18493 Mon Sep 17 00:00:00 2001 From: spla Date: Mon, 23 Nov 2020 11:42:04 +0100 Subject: [PATCH 07/51] Fix #7. Recoded check and checkmates warnings --- mastochess.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/mastochess.py b/mastochess.py index a969b20..fe7b24e 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1055,9 +1055,7 @@ if __name__ == '__main__': board.push(chess.Move.from_uci(moving)) - if board.is_check() == True: - - toot_text = "@"+playing_user + ": " + "@"+username + " t'ha fet escac!" + if bool(board.is_check()): if username == white_user: @@ -1073,16 +1071,28 @@ if __name__ == '__main__': game_moves = board.ply() - toot_text += "\nEscac i mat! (en " + str(game_moves) + " moviments)" + "\n\nEl guanyador és: " + "@"+username + '\n' + close_game() + + checkmate = True + + else: + + checkmate = False + + if check == True and checkmate == False: + + toot_text = "@"+playing_user + " " + username + " t'ha fet escac!\n" + + elif check == True and checkmate == True: + + toot_text = "\nEscac i mat! (en " + str(game_moves) + " moviments)" + "\n\nEl guanyador és: " + "@"+username + '\n' toot_text += "\n@"+playing_user + ": ben jugat!" + "\n" - close_game() + toot_text += "\nPartides guanyades:" + "\n" played_games, wins = get_stats(username) - toot_text += "\nPartides guanyades" + "\n" - toot_text += username + ": " + str(wins) + " de " + str(played_games) + "\n" played_games, wins = get_stats(playing_user) @@ -1091,11 +1101,11 @@ if __name__ == '__main__': else: - toot_text = "@"+playing_user + ' el teu torn.'+ '\n' + toot_text = "@"+playing_user + ' el teu torn.'+ '\n' - if capture: + if capture == True and checkmate == False: - toot_text += "\n* has perdut " + piece_name + "!\n" + toot_text += "\n* has perdut " + piece_name + "!\n" toot_text += '\n#escacs' + '\n' -- 2.34.1 From e7468ccaafbd654d61e3f05d2359a8e023b2674b Mon Sep 17 00:00:00 2001 From: spla Date: Mon, 23 Nov 2020 18:22:08 +0100 Subject: [PATCH 08/51] New feature! All moves saved to file in realtime with san anotation --- README.md | 3 +- mastochess.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8002757..c96845a 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,5 @@ Within Python Virtual Environment: ![board](board.png) 20.11.2020 - New feature! added link to on going games in games list -21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces +21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces +23.11.2020 - New feature! Now all moves are saved to file (with san anotation). diff --git a/mastochess.py b/mastochess.py index fe7b24e..dc70582 100644 --- a/mastochess.py +++ b/mastochess.py @@ -117,6 +117,34 @@ def get_piece_name(captured_piece): return piece_name +def get_moved_piece_name(moved_piece): + + if moved_piece == 1: + + moved_piece_name = "P" + + if moved_piece == 2: + + moved_piece_name = "C" + + if moved_piece == 3: + + moved_piece_name = "A" + + if moved_piece == 4: + + moved_piece_name = "T" + + if moved_piece == 5: + + moved_piece_name = "D" + + if moved_piece == 6: + + moved_piece_name = "R" + + return moved_piece_name + def get_notification_data(): try: @@ -475,6 +503,72 @@ def update_game(board_game, toot_url): conn.close() +def save_annotation(): + + square_index = chess.SQUARE_NAMES.index(moving[2:]) + + moved_piece = board.piece_type_at(square_index) + + moved_piece_name = get_moved_piece_name(moved_piece) + + game_file = "anotations/" + str(game_id) + + if moved_piece_name == 'P': + + moved_piece_name = moved_piece_name.replace('P','') + + if capture == True: + + moved_piece_name = moved_piece_name + "X" + + if check == True: + + moved_piece_name = moved_piece_name + moving[2:] + "+" + + if checkmate == True: + + moved_piece_name = moved_piece_name + "+" + + if bool(board.turn == chess.BLACK) == True: + + if check != True and checkmate != True: + + line_data = str(board.fullmove_number) + ". " + moved_piece_name + moving[2:] + + else: + + line_data = str(board.fullmove_number) + ". " + moved_piece_name + + else: + + moved_piece_name = moved_piece_name.lower() + + if check != True and checkmate != True: + + line_data = " - " + moved_piece_name + moving[2:] + "\n" + + else: + + line_data = " - " + moved_piece_name + "\n" + + if not os.path.isfile(game_file): + + file_header = "Partida: " + str(game_id) + "\n" + white_user + " / " + black_user + "\n\n" + + with open(game_file, 'w+') as f: + + f.write(file_header) + + with open(game_file, 'a') as f: + + f.write(line_data) + + else: + + with open(game_file, 'a') as f: + + f.write(line_data) + def close_game(): now = datetime.now() @@ -844,6 +938,8 @@ def get_parameter( parameter, file_path ): def create_dir(): if not os.path.exists('games'): os.makedirs('games') + if not os.path.exists('anotations'): + os.makedirs('anotations') def usage(): @@ -1145,6 +1241,8 @@ if __name__ == '__main__': game_moves = board.ply() + save_annotation() + update_moves(username, game_moves) update_replies(status_id, username, now) -- 2.34.1 From 61d6ffcbb69c2e9b0a8c975072fc985594025a5e Mon Sep 17 00:00:00 2001 From: spla Date: Tue, 24 Nov 2020 22:33:26 +0100 Subject: [PATCH 09/51] fix typo --- mastochess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mastochess.py b/mastochess.py index dc70582..33eb7d9 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1293,7 +1293,7 @@ if __name__ == '__main__': toot_text = "@"+username + " ha deixat la partida amb " + white_user - mastodon.statud_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) close_game() -- 2.34.1 From e158eb862e66334b4a4da5eeb60f5af823c39fb6 Mon Sep 17 00:00:00 2001 From: spla Date: Wed, 25 Nov 2020 11:38:16 +0100 Subject: [PATCH 10/51] New feature! %1 - send game anotations to players --- README.md | 9 ++- mastochess.py | 161 ++++++++++++++++++++++++++++++++++++++++++++++++-- smtp_setup.py | 55 +++++++++++++++++ 3 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 smtp_setup.py diff --git a/README.md b/README.md index c96845a..b55e907 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ To finish game at any time: To list on going games: -@your_bot_username games +@your_bot_username games + +To get any game anotations, in ex. game 1: + +@your_bot_username send 1 ### Dependencies @@ -41,6 +45,7 @@ Within Python Virtual Environment: ![board](board.png) -20.11.2020 - New feature! added link to on going games in games list +20.11.2020 - New feature! Added link to on going games in games list 21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces 23.11.2020 - New feature! Now all moves are saved to file (with san anotation). +25.11.2020 - New feature! Get any game anotations via email. diff --git a/mastochess.py b/mastochess.py index 33eb7d9..e480968 100644 --- a/mastochess.py +++ b/mastochess.py @@ -6,6 +6,14 @@ import re import unidecode from datetime import datetime, timedelta from mastodon import Mastodon +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders +import smtplib +from smtplib import SMTPException, SMTPAuthenticationError, SMTPConnectError, SMTPRecipientsRefused +import socket +from socket import gaierror import psycopg2 import chess import chess.svg @@ -159,7 +167,7 @@ def get_notification_data(): url_lst = [] - search_text = ['fi','mou','nova','jocs'] + search_text = ['fi','mou','nova','jocs', 'envia'] conn = None @@ -511,7 +519,7 @@ def save_annotation(): moved_piece_name = get_moved_piece_name(moved_piece) - game_file = "anotations/" + str(game_id) + game_file = "anotations/" + str(game_id) + ".txt" if moved_piece_name == 'P': @@ -569,6 +577,108 @@ def save_annotation(): f.write(line_data) +def get_email_address(username): + + try: + + conn = None + + conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("select email from users where account_id = (select id from accounts where username = (%s) and domain is null)", (username,)) + + row = cur.fetchone() + + username_email = row[0] + + cur.close() + + return username_email + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def send_anotation(game_id): + + emailed = False + + game_found = False + + username_email = get_email_address(username) + + # Create message object instance + msg = MIMEMultipart() + + # Declare message elements + msg['From'] = smtp_user_login + msg['To'] = username_email + msg['Subject'] = "Anotaciones partida n." + game_id + + # Attach the game anotation + file_to_attach = "anotations/" + game_id + ".txt" + try: + + attachment = open(file_to_attach, 'rb') + + game_found = True + + except FileNotFoundError as not_found_error: + + print(not_found_error) + return (emailed, game_id, game_found) + + obj = MIMEBase('application', 'octet-stream') + obj.set_payload(attachment.read()) + encoders.encode_base64(obj) + obj.add_header('Content-Disposition', "attachment; filename= "+file_to_attach) + + # Add the message body to the object instance + msg.attach(obj) + + try: + + # Create the server connection + server = smtplib.SMTP(smtp_host) + # Switch the connection over to TLS encryption + server.starttls() + # Authenticate with the server + server.login(smtp_user_login, smtp_user_password) + # Send the message + server.sendmail(msg['From'], msg['To'], msg.as_string()) + # Disconnect + server.quit() + + print("Successfully sent game anotations to %s" % msg['To']) + emailed = True + return (emailed, game_id, game_found) + + except SMTPAuthenticationError as auth_error: + + print(auth_error) + pass + return emailed + + except socket.gaierror as socket_error: + + print(socket_error) + pass + return emailed + + except SMTPRecipientsRefused as recip_error: + + print(recip_error) + pass + return emailed + def close_game(): now = datetime.now() @@ -874,6 +984,10 @@ def replying(): reply = True + elif query_word[0:5] == 'envia': + + reply = True + else: reply = False @@ -921,6 +1035,15 @@ def db_config(): return (mastodon_db, mastodon_db_user, chess_db, chess_db_user) +def smtp_config(): + + smtp_filepath = "config/smtp_config.txt" + smtp_host = get_parameter("smtp_host", smtp_filepath) + smtp_user_login = get_parameter("smtp_user_login", smtp_filepath) + smtp_user_password = get_parameter("smtp_user_password", smtp_filepath) + + return (smtp_host, smtp_user_login, smtp_user_password) + def get_parameter( parameter, file_path ): if not os.path.isfile(file_path): @@ -964,6 +1087,8 @@ if __name__ == '__main__': mastodon_db, mastodon_db_user, chess_db, chess_db_user = db_config() + smtp_host, smtp_user_login, smtp_user_password = smtp_config() + now = datetime.now() create_dir() @@ -1067,7 +1192,7 @@ if __name__ == '__main__': toot_text += '\n' - toot_text += '#escacs' + '\n' + toot_text += 'partida: ' + str(game_id) + ' ' + '#escacs' + '\n' image_id = mastodon.media_post(board_file, "image/png").id @@ -1079,6 +1204,30 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word[0:5] == 'envia': + + query_word_length = len(query_word) + + send_game = query_word[6:query_word_length].replace(' ', '') + + emailed, game_id, game_found = send_anotation(send_game) + + if emailed == False and game_found == True: + + toot_text = "@"+username + " error al enviar les anotacions :-(" + + elif emailed == True and game_found == True: + + toot_text = "@"+username + " les anotaciones de la partida n." + str(game_id) + " enviades amb èxit!" + + elif emailed == False and game_found == False: + + toot_text = "@"+username + " la partida n." + str(game_id) + " no existeix..." + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + else: update_replies(status_id, username, now) @@ -1099,7 +1248,7 @@ if __name__ == '__main__': toot_text += '\n' - toot_text += '#escacs' + '\n' + toot_text += 'partida: ' + str(game_id) + ' ' + '#escacs' + '\n' board = chess.Board(on_going_game) @@ -1203,7 +1352,7 @@ if __name__ == '__main__': toot_text += "\n* has perdut " + piece_name + "!\n" - toot_text += '\n#escacs' + '\n' + toot_text += '\npartida: ' + str(game_id) + ' ' + '#escacs' + '\n' if username == white_user: @@ -1370,7 +1519,7 @@ if __name__ == '__main__': toot_text += '\n' - toot_text += '#escacs' + '\n' + toot_text += 'partida: ' + str(game_id) + ' ' + '#escacs' + '\n' board = chess.Board(on_going_game) diff --git a/smtp_setup.py b/smtp_setup.py new file mode 100644 index 0000000..e27bba5 --- /dev/null +++ b/smtp_setup.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import getpass +import fileinput,re +import os +import sys + +def create_dir(): + if not os.path.exists('config'): + os.makedirs('config') + +def create_file(): + if not os.path.exists('config/smtp_config.txt'): + with open('config/smtp_config.txt', 'w'): pass + print(smtp_filepath + " created!") + +def write_parameter( parameter, smtp_filepath ): + print("\n") + print("Setting up SMTP parameters...") + smtp_host = input("Enter SMTP hostname: ") + smtp_user_login = input("Enter SMTP user login, ex. user@" + smtp_host +"? ") + smtp_user_password = getpass.getpass("SMTP user password? ") + with open(smtp_filepath, "w") as text_file: + print("smtp_host: {}".format(smtp_host), file=text_file) + print("smtp_user_login: {}".format(smtp_user_login), file=text_file) + print("smtp_user_password: {}".format(smtp_user_password), file=text_file) + +# Returns the parameter from the specified file +def get_parameter( parameter, smtp_filepath ): + # Check if file exists + if not os.path.isfile(smtp_filepath): + print("File %s not found, creating it."%smtp_filepath) + create_dir() + create_file() + write_parameter( parameter, smtp_filepath ) + print("\n") + print("SMTP setup done!\n") + + # Find parameter in file + with open( smtp_filepath ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + # Cannot find parameter, exit + print(file_path + " Missing parameter %s "%parameter) + sys.exit(0) + +# Load secrets from secrets file +smtp_filepath = "config/smtp_config.txt" +smtp_host = get_parameter("smtp_host", smtp_filepath) +smtp_user_login = get_parameter("smtp_user_login", smtp_filepath) +smtp_user_pass = get_parameter("smtp_user_pass", smtp_filepath) + -- 2.34.1 From b9277c532cd856844ada997ea37ea5b3269fab9c Mon Sep 17 00:00:00 2001 From: spla Date: Wed, 25 Nov 2020 11:40:36 +0100 Subject: [PATCH 11/51] New feature! Fix %1 - send game anotations to players --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b55e907..12a15f0 100644 --- a/README.md +++ b/README.md @@ -47,5 +47,5 @@ Within Python Virtual Environment: 20.11.2020 - New feature! Added link to on going games in games list 21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces -23.11.2020 - New feature! Now all moves are saved to file (with san anotation). +23.11.2020 - New feature! Now all moves are saved to file (with san anotation). 25.11.2020 - New feature! Get any game anotations via email. -- 2.34.1 From 741f817f2803d6c9d8e2c497768dbc6c01c78e9f Mon Sep 17 00:00:00 2001 From: spla Date: Wed, 25 Nov 2020 13:15:43 +0100 Subject: [PATCH 12/51] Get game anotations even if it's not finalized yet --- mastochess.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/mastochess.py b/mastochess.py index e480968..b9985d6 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1507,6 +1507,30 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word[0:5] == 'envia': + + query_word_length = len(query_word) + + send_game = query_word[6:query_word_length].replace(' ', '') + + emailed, game_id, game_found = send_anotation(send_game) + + if emailed == False and game_found == True: + + toot_text = "@"+username + " error al enviar les anotacions :-(" + + elif emailed == True and game_found == True: + + toot_text = "@"+username + " les anotaciones de la partida n." + str(game_id) + " enviades amb èxit!" + + elif emailed == False and game_found == False: + + toot_text = "@"+username + " la partida n." + str(game_id) + " no existeix..." + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + else: if playing_user == None: -- 2.34.1 From f04e7891e8a7b1d82016f1f88f0c6f73ee516971 Mon Sep 17 00:00:00 2001 From: spla Date: Wed, 25 Nov 2020 13:42:53 +0100 Subject: [PATCH 13/51] Updated --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 12a15f0..b881ac2 100644 --- a/README.md +++ b/README.md @@ -37,15 +37,17 @@ Within Python Virtual Environment: 1. Run `pip install -r requirements.txt` to install needed Python libraries. -2. Run `python db-setup.py` to setup and create new Postgresql database and needed tables in it. +2. Run `python db-setup.py` to setup and create new Postgresql database and needed tables in it. -3. Run `python setup.py` to get your Mastodon's bot account tokens. +3. Run `python smtp_setup.py` to setup your smtp server, user and password. Needed to send game anotations to players. -4. Use your favourite scheduling method to set `python mastochess.py` to run regularly. +4. Run `python setup.py` to get your Mastodon's bot account tokens. + +5. Use your favourite scheduling method to set `python mastochess.py` to run regularly. ![board](board.png) 20.11.2020 - New feature! Added link to on going games in games list 21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces 23.11.2020 - New feature! Now all moves are saved to file (with san anotation). -25.11.2020 - New feature! Get any game anotations via email. +25.11.2020 - New feature! Get any game anotations via email (see point 3 above). -- 2.34.1 From 2d43565417b29ceb60c8c861eabc1b807bac342a Mon Sep 17 00:00:00 2001 From: spla Date: Wed, 25 Nov 2020 18:34:45 +0100 Subject: [PATCH 14/51] Changed Queen piece name --- mastochess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mastochess.py b/mastochess.py index b9985d6..a8c2f38 100644 --- a/mastochess.py +++ b/mastochess.py @@ -117,7 +117,7 @@ def get_piece_name(captured_piece): if captured_piece == 5: - piece_name = "la Reina" + piece_name = "la Dama" if captured_piece == 6: -- 2.34.1 From 8ec306c6806ac54a8c3c685746fd4a1ab8d128c2 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 27 Nov 2020 20:50:38 +0100 Subject: [PATCH 15/51] Added pawn promotion and locales support! --- locales/cat.txt | 48 +++++++ locales/eng.txt | 48 +++++++ mastochess.py | 349 ++++++++++++++++++++++++++++++++++++------------ setup.py | 36 ----- 4 files changed, 360 insertions(+), 121 deletions(-) create mode 100644 locales/cat.txt create mode 100644 locales/eng.txt diff --git a/locales/cat.txt b/locales/cat.txt new file mode 100644 index 0000000..2c23c6c --- /dev/null +++ b/locales/cat.txt @@ -0,0 +1,48 @@ +search_end: fi +search_move: mou +search_new: nova +search_games: jocs +search_send: envia +new_game_started: partida iniciada! Esperant jugador... +playing_with: jugues amb +your_turn: el teu torn +game_name: partida +chess_hashtag: #escacs +send_error: error al enviar les anotacions :-( +game_number_anotations: les anotacions de la partida n. +anotations_sent: enviades amb èxit! +game_no_exists: la partida n. +it_not_exists: no existeix... +game_already_started: ja tenies iniciada una partida! +wait_other_player: espera l'altre jugador +is_not_legal_move: és un moviment il·legal. Torna a tirar. +check_done: t'ha fet escac! +check_mate: Escac i mat! (en +check_mate_movements: moviments) +the_winner_is: El guanyador és: +well_done: ben jugat! +winned_games: Partides guanyades: +wins_of_many: de +lost_piece: * has perdut +not_legal_move_str: moviment il·legal! +player_leave_game: ha deixat la partida amb +leave_waiting_game: has abandonat la partida en espera. +started_games: partides iniciades: +game_is_waiting: en espera... +game_is_on_going: (en joc) +no_on_going_games: cap partida en joc +is_not_your_turn: no és el teu torn. +is_the_turn_of: és el torn de +pawn_piece: un peó +knight_piece: un cavall +bishop_piece: l'alfil +rook_piece: una torre +queen_piece: la Dama +king_piece: el Rei +pawn_piece_letter: P +knight_piece_letter: C +bishop_piece_letter: A +rook_piece_letter: T +queen_piece_letter: D +king_piece_letter: R +email_subject: Anotacions partida n. diff --git a/locales/eng.txt b/locales/eng.txt new file mode 100644 index 0000000..508e755 --- /dev/null +++ b/locales/eng.txt @@ -0,0 +1,48 @@ +search_end: end +search_move: move +search_new: new +search_games: games +search_send: send +new_game_started: game started! Waiting for the second player... +playing_with: you play with +your_turn: it's your turn +game_name: game +chess_hashtag: #chess +send_error: sending anotations error :-( +game_number_anotations: the anotations of game n. +anotations_sent: succesfully sent! +game_no_exists: the game n. +it_not_exists: don't exists... +game_already_started: you already started a game! +wait_other_player: wait for the second player +is_not_legal_move: is not a legal move. Play again. +check_done: you are in check! +check_mate: it's a checkmate! (in +check_mate_movements: moves) +the_winner_is: The winner is: +well_done: well done! +winned_games: Won games: +wins_of_many: of +lost_piece: * you have lost +not_legal_move_str: not a legal move! +player_leave_game: has left the game with +leave_waiting_game: you have left the game in hold. +started_games: started games: +game_is_waiting: on hold... +game_is_on_going: (on going) +no_on_going_games: no games +is_not_your_turn: it's not your turn. +is_the_turn_of: it's the turn of +pawn_piece: a pawn +knight_piece: one knight +bishop_piece: one bishop +rook_piece: a rook +queen_piece: the Queen +king_piece: the King +pawn_piece_letter: P +knight_piece_letter: N +bishop_piece_letter: B +rook_piece_letter: R +queen_piece_letter: Q +king_piece_letter: K +email_subject: Anotations of game n. diff --git a/mastochess.py b/mastochess.py index a8c2f38..fff5d27 100644 --- a/mastochess.py +++ b/mastochess.py @@ -101,27 +101,27 @@ def get_piece_name(captured_piece): if captured_piece == 1: - piece_name = "un Peó" + piece_name = pawn_piece if captured_piece == 2: - piece_name = "un cavall" + piece_name = knight_piece if captured_piece == 3: - piece_name = "l'àlfil" + piece_name = bishop_piece if captured_piece == 4: - piece_name = "una torre" + piece_name = rook_piece if captured_piece == 5: - piece_name = "la Dama" + piece_name = queen_piece if captured_piece == 6: - piece_name = "el Rei" + piece_name = king_piece return piece_name @@ -129,27 +129,27 @@ def get_moved_piece_name(moved_piece): if moved_piece == 1: - moved_piece_name = "P" + moved_piece_name = pawn_piece_letter if moved_piece == 2: - moved_piece_name = "C" + moved_piece_name = knight_piece_letter if moved_piece == 3: - moved_piece_name = "A" + moved_piece_name = bishop_piece_letter if moved_piece == 4: - moved_piece_name = "T" + moved_piece_name = rook_piece_letter if moved_piece == 5: - moved_piece_name = "D" + moved_piece_name = queen_piece_letter if moved_piece == 6: - moved_piece_name = "R" + moved_piece_name = king_piece_letter return moved_piece_name @@ -167,7 +167,7 @@ def get_notification_data(): url_lst = [] - search_text = ['fi','mou','nova','jocs', 'envia'] + search_text = [search_end, search_move, search_new, search_games, search_send] conn = None @@ -511,13 +511,23 @@ def update_game(board_game, toot_url): conn.close() -def save_annotation(): +def save_anotation(moving): - square_index = chess.SQUARE_NAMES.index(moving[2:]) + if moving_piece != 1: - moved_piece = board.piece_type_at(square_index) + square_index = chess.SQUARE_NAMES.index(moving[2:]) - moved_piece_name = get_moved_piece_name(moved_piece) + moved_piece = board.piece_type_at(square_index) + + moved_piece_name = get_moved_piece_name(moved_piece) + + else: + + moved_piece_name = 'P' + + if promoted == True: + + moving = moving + 'q' game_file = "anotations/" + str(game_id) + ".txt" @@ -541,7 +551,13 @@ def save_annotation(): if check != True and checkmate != True: - line_data = str(board.fullmove_number) + ". " + moved_piece_name + moving[2:] + if promoted != True: + + line_data = str(board.fullmove_number) + ". " + moved_piece_name + moving[2:] + + else: + + line_data = str(board.fullmove_number) + ". " + moved_piece_name + "=D" else: @@ -553,15 +569,20 @@ def save_annotation(): if check != True and checkmate != True: - line_data = " - " + moved_piece_name + moving[2:] + "\n" + if promoted != True: + line_data = " - " + moved_piece_name + moving[2:] + "\n" + + else: + + line_data = " - " + moved_piece_name + "=D" else: line_data = " - " + moved_piece_name + "\n" if not os.path.isfile(game_file): - file_header = "Partida: " + str(game_id) + "\n" + white_user + " / " + black_user + "\n\n" + file_header = game_name + ': ' + str(game_id) + "\n" + white_user + " / " + black_user + "\n\n" with open(game_file, 'w+') as f: @@ -621,7 +642,7 @@ def send_anotation(game_id): # Declare message elements msg['From'] = smtp_user_login msg['To'] = username_email - msg['Subject'] = "Anotaciones partida n." + game_id + msg['Subject'] = email_subject + game_id # Attach the game anotation file_to_attach = "anotations/" + game_id + ".txt" @@ -967,30 +988,30 @@ def replying(): if unidecode.unidecode(question)[0:query_word_length] == query_word: - if query_word[0:4] == 'nova': + if query_word == search_new: - reply = True + reply = True - elif query_word[0:3] == 'mou': + elif query_word[:search_move_slicing] == search_move: - moving = query_word[4:query_word_length].replace(" ","") - reply = True + moving = query_word[4:query_word_length].replace(" ","") + reply = True - elif query_word[0:2] == 'fi': + elif query_word == search_end: - reply = True + reply = True - elif query_word[0:4] == 'jocs': + elif query_word == search_games: - reply = True + reply = True - elif query_word[0:5] == 'envia': + elif query_word[:search_send_slicing] == search_send: - reply = True + reply = True - else: + else: - reply = False + reply = False return (reply, query_word, moving) @@ -998,6 +1019,84 @@ def replying(): print(v_error) +def load_strings(bot_lang): + + search_end = get_parameter("search_end", language_filepath) + search_move = get_parameter("search_move", language_filepath) + search_new = get_parameter("search_new", language_filepath) + search_games = get_parameter("search_games", language_filepath) + search_send = get_parameter("search_send", language_filepath) + new_game_started = get_parameter("new_game_started", language_filepath) + playing_with = get_parameter("playing_with", language_filepath) + your_turn = get_parameter("your_turn", language_filepath) + game_name = get_parameter("game_name", language_filepath) + chess_hashtag = get_parameter("chess_hashtag", language_filepath) + send_error = get_parameter("send_error", language_filepath) + + return (search_end, search_move, search_new, search_games, search_send, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error) + +def load_strings1(bot_lang): + + game_number_anotations = get_parameter("game_number_anotations", language_filepath) + anotations_sent = get_parameter("anotations_sent", language_filepath) + game_no_exists = get_parameter("game_no_exists", language_filepath) + it_not_exists = get_parameter("it_not_exists", language_filepath) + game_already_started = get_parameter("game_already_started", language_filepath) + wait_other_player = get_parameter("wait_other_player", language_filepath) + is_not_legal_move = get_parameter("is_not_legal_move", language_filepath) + check_done = get_parameter("check_done", language_filepath) + check_mate = get_parameter("check_mate", language_filepath) + + return (game_number_anotations, anotations_sent, game_no_exists, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate) + +def load_strings2(bot_lang): + + check_mate_movements = get_parameter("check_mate_movements", language_filepath) + the_winner_is = get_parameter("the_winner_is", language_filepath) + well_done = get_parameter("well_done", language_filepath) + winned_games = get_parameter("winned_games", language_filepath) + wins_of_many = get_parameter("wins_of_many", language_filepath) + lost_piece = get_parameter("lost_piece", language_filepath) + not_legal_move_str = get_parameter("not_legal_move_str", language_filepath) + player_leave_game = get_parameter("player_leave_game", language_filepath) + + return (check_mate_movements, the_winner_is, well_done, winned_games, wins_of_many, lost_piece, not_legal_move_str, player_leave_game) + +def load_strings3(bot_lang): + + leave_waiting_game = get_parameter("leave_waiting_game", language_filepath) + started_games = get_parameter("started_games", language_filepath) + game_is_waiting = get_parameter("game_is_waiting", language_filepath) + game_is_on_going = get_parameter("game_is_on_going", language_filepath) + no_on_going_games = get_parameter("no_on_going_games", language_filepath) + is_not_your_turn = get_parameter("is_not_your_turn", language_filepath) + is_the_turn_of = get_parameter("is_the_turn_of", language_filepath) + + return (leave_waiting_game, started_games, game_is_waiting, game_is_on_going, no_on_going_games, is_not_your_turn, is_the_turn_of) + +def load_strings4(bot_lang): + + pawn_piece = get_parameter("pawn_piece", language_filepath) + knight_piece = get_parameter("knight_piece", language_filepath) + bishop_piece = get_parameter("bishop_piece", language_filepath) + rook_piece = get_parameter("rook_piece", language_filepath) + queen_piece = get_parameter("queen_piece", language_filepath) + king_piece = get_parameter("king_piece", language_filepath) + + return (pawn_piece, knight_piece, bishop_piece, rook_piece, queen_piece, king_piece) + +def load_strings5(bot_lang): + + pawn_piece_letter = get_parameter("pawn_piece_letter", language_filepath) + knight_piece_letter = get_parameter("knight_piece_letter", language_filepath) + bishop_piece_letter = get_parameter("bishop_piece_letter", language_filepath) + rook_piece_letter = get_parameter("rook_piece_letter", language_filepath) + queen_piece_letter = get_parameter("queen_piece_letter", language_filepath) + king_piece_letter = get_parameter("king_piece_letter", language_filepath) + email_subject = get_parameter("email_subject", language_filepath) + + return (pawn_piece_letter, knight_piece_letter, bishop_piece_letter, rook_piece_letter, queen_piece_letter, king_piece_letter, email_subject) + def mastodon(): # Load secrets from secrets file @@ -1066,7 +1165,7 @@ def create_dir(): def usage(): - print('usage: python ' + sys.argv[0] + ' --play') + print('usage: python ' + sys.argv[0] + ' --play' + ' --eng') ############################################################################### # main @@ -1079,10 +1178,58 @@ if __name__ == '__main__': usage() - elif len(sys.argv) == 2: + elif len(sys.argv) >= 2: if sys.argv[1] == '--play': + if len(sys.argv) == 3: + + if sys.argv[2] == '--eng': + + bot_lang = 'eng' + else: + + bot_lang = 'ca' + + if bot_lang == "ca": + + language_filepath = "locales/cat.txt" + + elif bot_lang == "eng": + + language_filepath = "locales/eng.txt" + + else: + + print("\nOnly 'cat' and 'eng' languages are supported.\n") + sys.exit(0) + + + + if bot_lang == 'ca': + + search_move_slicing = 3 + search_send_slicing = 5 + send_game_slicing = 6 + + elif bot_lang == 'eng': + + search_move_slicing = 4 + search_send_slicing = 4 + send_game_slicing = 5 + + search_end, search_move, search_new, search_games, search_send, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error = load_strings(bot_lang) + + game_number_anotations, anotations_sent, game_no_exists, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate = load_strings1(bot_lang) + + check_mate_movements, the_winner_is, well_done, winned_games, wins_of_many, lost_piece, not_legal_move_str, player_leave_game = load_strings2(bot_lang) + + leave_waiting_game, started_games, game_is_waiting, game_is_on_going, no_on_going_games, is_not_your_turn, is_the_turn_of = load_strings3(bot_lang) + + pawn_piece, knight_piece, bishop_piece, rook_piece, queen_piece, king_piece = load_strings4(bot_lang) + + pawn_piece_letter, knight_piece_letter, bishop_piece_letter, rook_piece_letter, queen_piece_letter, king_piece_letter, email_subject = load_strings5(bot_lang) + mastodon, mastodon_hostname, bot_username = mastodon() mastodon_db, mastodon_db_user, chess_db, chess_db_user = db_config() @@ -1130,7 +1277,7 @@ if __name__ == '__main__': status_url = url_lst[i] - if query_word != "jocs": + if query_word != search_games: is_playing, game_id, white_user, black_user, on_going_game, waiting, playing_user = check_games() @@ -1144,7 +1291,7 @@ if __name__ == '__main__': if reply == True and is_playing == False: - if query_word == 'nova' and not game_waiting: + if query_word == search_new and not game_waiting: board = chess.Board() @@ -1154,7 +1301,7 @@ if __name__ == '__main__': svg2png(bytestring=svgfile,write_to=board_file) - toot_text = "@"+username+ " partida iniciada! Esperant jugador... " +"\n" + toot_text = '@'+username + ' ' + new_game_started + '\n' toot_text += '\n' @@ -1168,7 +1315,7 @@ if __name__ == '__main__': update_replies(status_id, username, now) - elif query_word == 'nova' and game_waiting: + elif query_word == search_new and game_waiting: game_status, white_user, chess_game = join_player() @@ -1184,15 +1331,15 @@ if __name__ == '__main__': svg2png(bytestring=svgfile,write_to=board_file) - toot_text = "@"+username + " jugues amb " + white_user + "\n" + toot_text = '@'+username + ' ' + playing_with + ' ' + white_user + "\n" toot_text += '\n' - toot_text += "@"+white_user + ": el teu torn" + "\n" + toot_text += '@'+white_user + ': ' + your_turn + "\n" toot_text += '\n' - toot_text += 'partida: ' + str(game_id) + ' ' + '#escacs' + '\n' + toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' image_id = mastodon.media_post(board_file, "image/png").id @@ -1204,25 +1351,25 @@ if __name__ == '__main__': update_replies(status_id, username, now) - elif query_word[0:5] == 'envia': + elif query_word[:search_send_slicing] == search_send: query_word_length = len(query_word) - send_game = query_word[6:query_word_length].replace(' ', '') + send_game = query_word[send_game_slicing:query_word_length].replace(' ', '') emailed, game_id, game_found = send_anotation(send_game) if emailed == False and game_found == True: - toot_text = "@"+username + " error al enviar les anotacions :-(" + toot_text = '@'+username + send_error elif emailed == True and game_found == True: - toot_text = "@"+username + " les anotaciones de la partida n." + str(game_id) + " enviades amb èxit!" + toot_text = '@'+username + ' ' + game_number_anotations + str(game_id) + ' ' + anotations_sent elif emailed == False and game_found == False: - toot_text = "@"+username + " la partida n." + str(game_id) + " no existeix..." + toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1234,9 +1381,9 @@ if __name__ == '__main__': elif reply and is_playing: - if query_word == 'nova': + if query_word == search_new: - toot_text = "@"+username + ' ja tenies iniciada una partida!' + '\n' + toot_text = '@'+username + ' ' + game_already_started + '\n' if black_user != '': @@ -1244,11 +1391,11 @@ if __name__ == '__main__': else: - toot_text += "espera l'altre jugador" + '\n' + toot_text += wait_other_player + '\n' toot_text += '\n' - toot_text += 'partida: ' + str(game_id) + ' ' + '#escacs' + '\n' + toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' board = chess.Board(on_going_game) @@ -1264,15 +1411,47 @@ if __name__ == '__main__': update_replies(status_id, username, now) - elif query_word[0:3] == 'mou' and playing_user == username: + elif query_word[:search_move_slicing] == search_move and playing_user == username: board = chess.Board(on_going_game) + promoted = False + try: - if chess.Move.from_uci(moving) not in board.legal_moves: + piece_square_index = chess.SQUARE_NAMES.index(moving[:2]) - toot_text = "@"+username + ": " + moving + " és un moviment il·legal. Torna a tirar." + "\n" + moving_piece = board.piece_type_at(piece_square_index) + + if moving_piece == 1: + + square_index = chess.SQUARE_NAMES.index(moving[2:4]) + + if bool(board.turn == chess.WHITE) == True: + + square_rank_trigger = 7 + + elif bool(board.turn == chess.BLACK) == True: + + square_rank_trigger = 0 + + if chess.square_rank(square_index) == square_rank_trigger: + + promoted = True + + if len(moving) == 4: + + moving = moving + 'q' + + not_legal_move = chess.Move.from_uci(moving) not in board.legal_moves + + else: + + not_legal_move = chess.Move.from_uci(moving) not in board.legal_moves + + if not_legal_move: + + toot_text = '@'+username + ': ' + moving + ' ' + is_not_legal_move + '\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1288,7 +1467,7 @@ if __name__ == '__main__': capture = True - square_capture_index = chess.SQUARE_NAMES.index(moving[2:]) + square_capture_index = chess.SQUARE_NAMES.index(moving[2:4]) captured_piece = board.piece_type_at(square_capture_index) @@ -1326,33 +1505,33 @@ if __name__ == '__main__': if check == True and checkmate == False: - toot_text = "@"+playing_user + " " + username + " t'ha fet escac!\n" + toot_text = "@"+playing_user + " " + username + ' ' + check_done + '\n' elif check == True and checkmate == True: - toot_text = "\nEscac i mat! (en " + str(game_moves) + " moviments)" + "\n\nEl guanyador és: " + "@"+username + '\n' + toot_text = '\n' + check_mate + ' ' + str(game_moves) + ' ' + check_mate_movements + '\n\n' + the_winner_is + ' ' + "@"+username + '\n' - toot_text += "\n@"+playing_user + ": ben jugat!" + "\n" + toot_text += "\n@"+playing_user + ': ' + well_done + "\n" - toot_text += "\nPartides guanyades:" + "\n" + toot_text += '\n' + winned_games + "\n" played_games, wins = get_stats(username) - toot_text += username + ": " + str(wins) + " de " + str(played_games) + "\n" + toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" played_games, wins = get_stats(playing_user) - toot_text += playing_user + ": " + str(wins) + " de " + str(played_games) + "\n" + toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" else: - toot_text = "@"+playing_user + ' el teu torn.'+ '\n' + toot_text = '@'+playing_user + ' ' + your_turn + '\n' if capture == True and checkmate == False: - toot_text += "\n* has perdut " + piece_name + "!\n" + toot_text += '\n' + lost_piece + ' ' + piece_name + '!\n' - toot_text += '\npartida: ' + str(game_id) + ' ' + '#escacs' + '\n' + toot_text += '\n' + game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' if username == white_user: @@ -1390,7 +1569,7 @@ if __name__ == '__main__': game_moves = board.ply() - save_annotation() + save_anotation(moving) update_moves(username, game_moves) @@ -1400,7 +1579,7 @@ if __name__ == '__main__': print(v_error) - toot_text = "@"+username + ' moviment il·legal! (' + moving + '!?)\n' + toot_text = '@'+username + ' ' + not_legal_move_str + moving + '!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1412,7 +1591,7 @@ if __name__ == '__main__': print(a_error) - toot_text = "@"+username + ' moviment il·legal! (' + moving + '!?)\n' + toot_text = '@'+username + ' ' + not_legal_move_str + moving + '!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1420,13 +1599,13 @@ if __name__ == '__main__': pass - elif query_word[0:2] == 'fi': + elif query_word == search_end: if black_user != '': if username == white_user: - toot_text = "@"+username + " ha deixat la partida amb " + "@"+black_user + toot_text = '@'+username + ' ' + player_leave_game + ' ' + '@'+black_user mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1440,7 +1619,7 @@ if __name__ == '__main__': else: - toot_text = "@"+username + " ha deixat la partida amb " + white_user + toot_text = '@'+username + ' ' + player_leave_game + ' ' + white_user mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1454,7 +1633,7 @@ if __name__ == '__main__': else: - toot_text = "@"+username + " has abandonat la partida en espera." + toot_text = '@'+username + ' ' + leave_waiting_game mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) @@ -1466,30 +1645,30 @@ if __name__ == '__main__': continue - elif query_word == 'jocs': + elif query_word == search_games: player1_name_lst, player2_name_lst, game_status_lst, game_link_lst, next_move_lst = current_games() if len(player1_name_lst) > 0: - toot_text = "@"+username + " partides iniciades:" + "\n" + toot_text = "@"+username + ' ' + started_games + "\n" i = 0 while i < len(player1_name_lst): if game_status_lst[i] == 'waiting': - toot_text += "\n" + player1_name_lst[i] + " / " + player2_name_lst[i] + " (en espera...)" + "\n" + toot_text += '\n' + player1_name_lst[i] + ' / ' + player2_name_lst[i] + ' ' + game_is_waiting + "\n" else: if next_move_lst[i] == player1_name_lst[i]: - toot_text += "\n*" + player1_name_lst[i] + " / " + player2_name_lst[i] + " (en joc)" + "\n" + toot_text += '\n*' + player1_name_lst[i] + ' / ' + player2_name_lst[i] + ' ' + game_is_on_going + '\n' else: - toot_text += "\n" + player1_name_lst[i] + " / *" + player2_name_lst[i] + " (en joc)" + "\n" + toot_text += '\n' + player1_name_lst[i] + ' / *' + player2_name_lst[i] + ' ' + game_is_on_going + '\n' if game_link_lst[i] != None: @@ -1501,13 +1680,13 @@ if __name__ == '__main__': else: - toot_text = "@"+username + " cap partida en joc" + "\n" + toot_text = '@'+username + ' ' + no_on_going_games + '\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) update_replies(status_id, username, now) - elif query_word[0:5] == 'envia': + elif query_word[:search_send_slicing] == search_send: query_word_length = len(query_word) @@ -1517,15 +1696,15 @@ if __name__ == '__main__': if emailed == False and game_found == True: - toot_text = "@"+username + " error al enviar les anotacions :-(" + toot_text = '@'+username + ' ' + send_error elif emailed == True and game_found == True: - toot_text = "@"+username + " les anotaciones de la partida n." + str(game_id) + " enviades amb èxit!" + toot_text = '@'+username + ' ' + game_number_anotations + str(game_id) + ' ' + anotations_sent elif emailed == False and game_found == False: - toot_text = "@"+username + " la partida n." + str(game_id) + " no existeix..." + toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1535,15 +1714,15 @@ if __name__ == '__main__': if playing_user == None: - toot_text = "@"+username + " no és el teu torn." + "\n" + toot_text = '@'+username + ' ' + is_not_your_turn + '\n' else: - toot_text = "@"+username + " és el torn de " + playing_user + "\n" + toot_text = '@'+username + ' ' + is_the_turn_of + ' ' + playing_user + "\n" toot_text += '\n' - toot_text += 'partida: ' + str(game_id) + ' ' + '#escacs' + '\n' + toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' board = chess.Board(on_going_game) diff --git a/setup.py b/setup.py index 105383c..fe3c7bd 100644 --- a/setup.py +++ b/setup.py @@ -39,11 +39,6 @@ def write_config(): print("Writing parameters names 'mastodon_hostname' and 'bot_username' to " + config_filepath) the_file.write('mastodon_hostname: \n' + 'bot_username: \n') -def write_lang_config(): - with open(lang_config_filepath, 'a') as lang_file: - lang_file.write('bot_lang: \n') - print("adding Bot lang parameter name 'bot_lang' to "+ lang_config_filepath) - def read_client_lines(self): client_path = 'app_clientcred.txt' with open(client_path) as fp: @@ -72,12 +67,6 @@ def read_config_line(): modify_file(config_filepath, "mastodon_hostname: ", value=hostname) modify_file(config_filepath, "bot_username: ", value=bot_username) -def read_lang_config_line(): - with open(lang_config_filepath) as fp: - line = fp.readline() - lang = input("Enter Bot lang, ex. en or ca: ") - modify_file(lang_config_filepath, "bot_lang: ", value=lang) - def log_in(): error = 0 try: @@ -195,27 +184,6 @@ def get_hostname( parameter, config_filepath ): print("setup done!") sys.exit(0) -# Returns the parameter from the specified file -def get_lang( parameter, lang_config_filepath ): - - # Check if lang file exists - if not os.path.isfile(lang_config_filepath): - print("File %s not found, creating it."%lang_config_filepath) - create_lang_config() - - # Find parameter in file - with open( lang_config_filepath ) as f: - for line in f: - if line.startswith( parameter ): - return line.replace(parameter + ":", "").strip() - - # Cannot find parameter, exit - print(lang_config_filepath + " Missing parameter %s "%parameter) - write_lang_config() - read_lang_config_line() - print("Bot lang setup done!") - sys.exit(0) - ############################################################################### # main @@ -232,7 +200,3 @@ if __name__ == '__main__': mastodon_hostname = get_hostname("mastodon_hostname", config_filepath) bot_username = get_parameter("bot_username", config_filepath) - # Load Bot lang from config file - lang_config_filepath = "config/lang_config.txt" - bot_lang = get_lang("bot_lang", lang_config_filepath) # E.g., en or ca - -- 2.34.1 From ad000a73feed49e5b701c2b74751b26a33b39800 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 27 Nov 2020 21:08:51 +0100 Subject: [PATCH 16/51] fix indent --- mastochess.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mastochess.py b/mastochess.py index fff5d27..e0486f4 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1191,18 +1191,18 @@ if __name__ == '__main__': bot_lang = 'ca' - if bot_lang == "ca": + if bot_lang == "ca": - language_filepath = "locales/cat.txt" + language_filepath = "locales/cat.txt" - elif bot_lang == "eng": + elif bot_lang == "eng": - language_filepath = "locales/eng.txt" + language_filepath = "locales/eng.txt" - else: + else: - print("\nOnly 'cat' and 'eng' languages are supported.\n") - sys.exit(0) + print("\nOnly 'cat' and 'eng' languages are supported.\n") + sys.exit(0) -- 2.34.1 From f8f25577c25d344d0179162cf1d4b7dbdae3d1ad Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 27 Nov 2020 22:13:16 +0100 Subject: [PATCH 17/51] Added pawn promotion and locales support! --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b881ac2..ce97e1b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,21 @@ To get any game anotations, in ex. game 1: @your_bot_username send 1 +To promote a pawn use first letter of desired piece: + +@your_bot_username move g7g8r (if you want a rook) + +n = knight +b = bishop +r = rook + +Don't use q for queen. Pawn is promoted to Queen by default. + + + + + + ### Dependencies - **Python 3** @@ -50,4 +65,5 @@ Within Python Virtual Environment: 20.11.2020 - New feature! Added link to on going games in games list 21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces 23.11.2020 - New feature! Now all moves are saved to file (with san anotation). -25.11.2020 - New feature! Get any game anotations via email (see point 3 above). +25.11.2020 - New feature! Get any game anotations via email (see point 3 above). +27.11.2020 - New feature! Pawn promotion and locales support ( ca & eng ) -- 2.34.1 From f3b7af3e82fcde5a349c79a640d85776d907c394 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 18:07:52 +0100 Subject: [PATCH 18/51] Added 'es' locale! --- mastochess.py | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/mastochess.py b/mastochess.py index e0486f4..96ca4a1 100644 --- a/mastochess.py +++ b/mastochess.py @@ -994,7 +994,7 @@ def replying(): elif query_word[:search_move_slicing] == search_move: - moving = query_word[4:query_word_length].replace(" ","") + moving = query_word[moving_slicing:query_word_length].replace(" ","") reply = True elif query_word == search_end: @@ -1165,7 +1165,7 @@ def create_dir(): def usage(): - print('usage: python ' + sys.argv[0] + ' --play' + ' --eng') + print('usage: python ' + sys.argv[0] + ' --play' + ' --en') ############################################################################### # main @@ -1184,40 +1184,55 @@ if __name__ == '__main__': if len(sys.argv) == 3: - if sys.argv[2] == '--eng': + if sys.argv[2] == '--en': - bot_lang = 'eng' + bot_lang = 'en' + + elif sys.argv[2] == '--es': + + bot_lang = 'es' else: bot_lang = 'ca' - if bot_lang == "ca": + if bot_lang == 'ca': - language_filepath = "locales/cat.txt" + language_filepath = 'locales/ca.txt' - elif bot_lang == "eng": + elif bot_lang == 'en': - language_filepath = "locales/eng.txt" + language_filepath = 'locales/en.txt' + + elif bot_lang == 'es': + + language_filepath = 'locales/es.txt' else: - print("\nOnly 'cat' and 'eng' languages are supported.\n") + print("\nOnly 'ca', 'es' and 'en' languages are supported.\n") sys.exit(0) - - if bot_lang == 'ca': search_move_slicing = 3 + moving_slicing = 3 search_send_slicing = 5 send_game_slicing = 6 - elif bot_lang == 'eng': + elif bot_lang == 'en': search_move_slicing = 4 + moving_slicing = 4 search_send_slicing = 4 send_game_slicing = 5 + elif bot_lang == 'es': + + search_move_slicing = 5 + moving_slicing = 5 + search_send_slicing = 5 + send_game_slicing = 6 + search_end, search_move, search_new, search_games, search_send, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error = load_strings(bot_lang) game_number_anotations, anotations_sent, game_no_exists, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate = load_strings1(bot_lang) @@ -1690,7 +1705,7 @@ if __name__ == '__main__': query_word_length = len(query_word) - send_game = query_word[6:query_word_length].replace(' ', '') + send_game = query_word[search_send_slicing:query_word_length].replace(' ', '') emailed, game_id, game_found = send_anotation(send_game) @@ -1747,3 +1762,6 @@ if __name__ == '__main__': i += 1 + else: + + usage() -- 2.34.1 From 7ba522f120ad250850bf08605e573cfa5219bbfc Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 18:09:48 +0100 Subject: [PATCH 19/51] Added 'es' locale! --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ce97e1b..451ee16 100644 --- a/README.md +++ b/README.md @@ -66,4 +66,5 @@ Within Python Virtual Environment: 21.11.2020 - New feature! Added a warning to player in turn when has been captured one of its pieces 23.11.2020 - New feature! Now all moves are saved to file (with san anotation). 25.11.2020 - New feature! Get any game anotations via email (see point 3 above). -27.11.2020 - New feature! Pawn promotion and locales support ( ca & eng ) +27.11.2020 - New feature! Pawn promotion and locales support ( ca & eng ) +28.11.2020 - New feature! Added 'es' locale support -- 2.34.1 From 01f51033e48aebfac73a060928f8e7d11c7187d3 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 18:10:31 +0100 Subject: [PATCH 20/51] Added 'es' locale! --- locales/es.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 locales/es.txt diff --git a/locales/es.txt b/locales/es.txt new file mode 100644 index 0000000..60f02e5 --- /dev/null +++ b/locales/es.txt @@ -0,0 +1,48 @@ +search_end: fin +search_move: mueve +search_new: nueva +search_games: partidas +search_send: envia +new_game_started: partida iniciada! Esperando jugador... +playing_with: juegas con +your_turn: tu turno +game_name: partida +chess_hashtag: #ajedrez +send_error: error al enviar las anotaciones :-( +game_number_anotations: las anotaciones de la partida n. +anotations_sent: enviades con éxito! +game_no_exists: la partida n. +it_not_exists: no existe... +game_already_started: ja tenias iniciada una partida! +wait_other_player: espera al otro jugador +is_not_legal_move: es un movimiento ilegal. Vuelve a tirar. +check_done: te ha echo jaque! +check_mate: Jaque mate! (en +check_mate_movements: movimientos) +the_winner_is: El ganador es: +well_done: bien jugado! +winned_games: Partidas ganadas: +wins_of_many: de +lost_piece: * has perdido +not_legal_move_str: movimiento ilegal! +player_leave_game: ha dejado la partida con +leave_waiting_game: has abandonado la partida en espera. +started_games: partidas iniciadas: +game_is_waiting: en espera... +game_is_on_going: (en juego) +no_on_going_games: ninguna partida en juego +is_not_your_turn: no es tu turno. +is_the_turn_of: es el turno de +pawn_piece: un peón +knight_piece: un caballo +bishop_piece: el alfil +rook_piece: una torre +queen_piece: la Dama +king_piece: el Rey +pawn_piece_letter: P +knight_piece_letter: C +bishop_piece_letter: A +rook_piece_letter: T +queen_piece_letter: D +king_piece_letter: R +email_subject: Anotaciones partida n. -- 2.34.1 From 340dc4182868255f90c4bf4b39f2f2ca57749792 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 18:12:55 +0100 Subject: [PATCH 21/51] Change file name to standard --- locales/ca.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ locales/en.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 locales/ca.txt create mode 100644 locales/en.txt diff --git a/locales/ca.txt b/locales/ca.txt new file mode 100644 index 0000000..2c23c6c --- /dev/null +++ b/locales/ca.txt @@ -0,0 +1,48 @@ +search_end: fi +search_move: mou +search_new: nova +search_games: jocs +search_send: envia +new_game_started: partida iniciada! Esperant jugador... +playing_with: jugues amb +your_turn: el teu torn +game_name: partida +chess_hashtag: #escacs +send_error: error al enviar les anotacions :-( +game_number_anotations: les anotacions de la partida n. +anotations_sent: enviades amb èxit! +game_no_exists: la partida n. +it_not_exists: no existeix... +game_already_started: ja tenies iniciada una partida! +wait_other_player: espera l'altre jugador +is_not_legal_move: és un moviment il·legal. Torna a tirar. +check_done: t'ha fet escac! +check_mate: Escac i mat! (en +check_mate_movements: moviments) +the_winner_is: El guanyador és: +well_done: ben jugat! +winned_games: Partides guanyades: +wins_of_many: de +lost_piece: * has perdut +not_legal_move_str: moviment il·legal! +player_leave_game: ha deixat la partida amb +leave_waiting_game: has abandonat la partida en espera. +started_games: partides iniciades: +game_is_waiting: en espera... +game_is_on_going: (en joc) +no_on_going_games: cap partida en joc +is_not_your_turn: no és el teu torn. +is_the_turn_of: és el torn de +pawn_piece: un peó +knight_piece: un cavall +bishop_piece: l'alfil +rook_piece: una torre +queen_piece: la Dama +king_piece: el Rei +pawn_piece_letter: P +knight_piece_letter: C +bishop_piece_letter: A +rook_piece_letter: T +queen_piece_letter: D +king_piece_letter: R +email_subject: Anotacions partida n. diff --git a/locales/en.txt b/locales/en.txt new file mode 100644 index 0000000..508e755 --- /dev/null +++ b/locales/en.txt @@ -0,0 +1,48 @@ +search_end: end +search_move: move +search_new: new +search_games: games +search_send: send +new_game_started: game started! Waiting for the second player... +playing_with: you play with +your_turn: it's your turn +game_name: game +chess_hashtag: #chess +send_error: sending anotations error :-( +game_number_anotations: the anotations of game n. +anotations_sent: succesfully sent! +game_no_exists: the game n. +it_not_exists: don't exists... +game_already_started: you already started a game! +wait_other_player: wait for the second player +is_not_legal_move: is not a legal move. Play again. +check_done: you are in check! +check_mate: it's a checkmate! (in +check_mate_movements: moves) +the_winner_is: The winner is: +well_done: well done! +winned_games: Won games: +wins_of_many: of +lost_piece: * you have lost +not_legal_move_str: not a legal move! +player_leave_game: has left the game with +leave_waiting_game: you have left the game in hold. +started_games: started games: +game_is_waiting: on hold... +game_is_on_going: (on going) +no_on_going_games: no games +is_not_your_turn: it's not your turn. +is_the_turn_of: it's the turn of +pawn_piece: a pawn +knight_piece: one knight +bishop_piece: one bishop +rook_piece: a rook +queen_piece: the Queen +king_piece: the King +pawn_piece_letter: P +knight_piece_letter: N +bishop_piece_letter: B +rook_piece_letter: R +queen_piece_letter: Q +king_piece_letter: K +email_subject: Anotations of game n. -- 2.34.1 From 10092da1f178ee4f2101cd4e116bd6c39b4664d1 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 18:16:41 +0100 Subject: [PATCH 22/51] Deleted no standard names files --- locales/cat.txt | 48 ------------------------------------------------ locales/eng.txt | 48 ------------------------------------------------ 2 files changed, 96 deletions(-) delete mode 100644 locales/cat.txt delete mode 100644 locales/eng.txt diff --git a/locales/cat.txt b/locales/cat.txt deleted file mode 100644 index 2c23c6c..0000000 --- a/locales/cat.txt +++ /dev/null @@ -1,48 +0,0 @@ -search_end: fi -search_move: mou -search_new: nova -search_games: jocs -search_send: envia -new_game_started: partida iniciada! Esperant jugador... -playing_with: jugues amb -your_turn: el teu torn -game_name: partida -chess_hashtag: #escacs -send_error: error al enviar les anotacions :-( -game_number_anotations: les anotacions de la partida n. -anotations_sent: enviades amb èxit! -game_no_exists: la partida n. -it_not_exists: no existeix... -game_already_started: ja tenies iniciada una partida! -wait_other_player: espera l'altre jugador -is_not_legal_move: és un moviment il·legal. Torna a tirar. -check_done: t'ha fet escac! -check_mate: Escac i mat! (en -check_mate_movements: moviments) -the_winner_is: El guanyador és: -well_done: ben jugat! -winned_games: Partides guanyades: -wins_of_many: de -lost_piece: * has perdut -not_legal_move_str: moviment il·legal! -player_leave_game: ha deixat la partida amb -leave_waiting_game: has abandonat la partida en espera. -started_games: partides iniciades: -game_is_waiting: en espera... -game_is_on_going: (en joc) -no_on_going_games: cap partida en joc -is_not_your_turn: no és el teu torn. -is_the_turn_of: és el torn de -pawn_piece: un peó -knight_piece: un cavall -bishop_piece: l'alfil -rook_piece: una torre -queen_piece: la Dama -king_piece: el Rei -pawn_piece_letter: P -knight_piece_letter: C -bishop_piece_letter: A -rook_piece_letter: T -queen_piece_letter: D -king_piece_letter: R -email_subject: Anotacions partida n. diff --git a/locales/eng.txt b/locales/eng.txt deleted file mode 100644 index 508e755..0000000 --- a/locales/eng.txt +++ /dev/null @@ -1,48 +0,0 @@ -search_end: end -search_move: move -search_new: new -search_games: games -search_send: send -new_game_started: game started! Waiting for the second player... -playing_with: you play with -your_turn: it's your turn -game_name: game -chess_hashtag: #chess -send_error: sending anotations error :-( -game_number_anotations: the anotations of game n. -anotations_sent: succesfully sent! -game_no_exists: the game n. -it_not_exists: don't exists... -game_already_started: you already started a game! -wait_other_player: wait for the second player -is_not_legal_move: is not a legal move. Play again. -check_done: you are in check! -check_mate: it's a checkmate! (in -check_mate_movements: moves) -the_winner_is: The winner is: -well_done: well done! -winned_games: Won games: -wins_of_many: of -lost_piece: * you have lost -not_legal_move_str: not a legal move! -player_leave_game: has left the game with -leave_waiting_game: you have left the game in hold. -started_games: started games: -game_is_waiting: on hold... -game_is_on_going: (on going) -no_on_going_games: no games -is_not_your_turn: it's not your turn. -is_the_turn_of: it's the turn of -pawn_piece: a pawn -knight_piece: one knight -bishop_piece: one bishop -rook_piece: a rook -queen_piece: the Queen -king_piece: the King -pawn_piece_letter: P -knight_piece_letter: N -bishop_piece_letter: B -rook_piece_letter: R -queen_piece_letter: Q -king_piece_letter: K -email_subject: Anotations of game n. -- 2.34.1 From 5c7849342c1d92018aa348c2a326dfca55a3bc55 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 18:35:31 +0100 Subject: [PATCH 23/51] Added help table --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 451ee16..4fb26fe 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,13 @@ r = rook Don't use q for queen. Pawn is promoted to Queen by default. - - - - +| ca | en | es | ex. | Observ. | +|:-----:|:-----:|:--------:|:----:|:-----------:| +| nova | new | nueva | | | +| mou | move | mueve | e2e3 | | +| fi | end | fin | | | +| jocs | games | partidas | | | +| envia | send | envia | 1 | game number | ### Dependencies -- 2.34.1 From 794beece5112fdd70a70020efba5e8be604a8fb1 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 20:12:36 +0100 Subject: [PATCH 24/51] Added fediverse support! Any fediverse user can play Mastodon Chess! --- locales/ca.txt | 1 + locales/en.txt | 1 + locales/es.txt | 1 + mastochess.py | 37 +++++++++++++++++++++++++++++-------- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/locales/ca.txt b/locales/ca.txt index 2c23c6c..439a2c4 100644 --- a/locales/ca.txt +++ b/locales/ca.txt @@ -12,6 +12,7 @@ send_error: error al enviar les anotacions :-( game_number_anotations: les anotacions de la partida n. anotations_sent: enviades amb èxit! game_no_exists: la partida n. +cant_send_to_fediverse_account: per ara no és possible :-( it_not_exists: no existeix... game_already_started: ja tenies iniciada una partida! wait_other_player: espera l'altre jugador diff --git a/locales/en.txt b/locales/en.txt index 508e755..6387db3 100644 --- a/locales/en.txt +++ b/locales/en.txt @@ -12,6 +12,7 @@ send_error: sending anotations error :-( game_number_anotations: the anotations of game n. anotations_sent: succesfully sent! game_no_exists: the game n. +cant_send_to_fediverse_account: not possible yet :-( it_not_exists: don't exists... game_already_started: you already started a game! wait_other_player: wait for the second player diff --git a/locales/es.txt b/locales/es.txt index 60f02e5..6cbd5b1 100644 --- a/locales/es.txt +++ b/locales/es.txt @@ -12,6 +12,7 @@ send_error: error al enviar las anotaciones :-( game_number_anotations: las anotaciones de la partida n. anotations_sent: enviades con éxito! game_no_exists: la partida n. +cant_send_to_fediverse_account: por ahora no es posible :-( it_not_exists: no existe... game_already_started: ja tenias iniciada una partida! wait_other_player: espera al otro jugador diff --git a/mastochess.py b/mastochess.py index 96ca4a1..4f7b0cc 100644 --- a/mastochess.py +++ b/mastochess.py @@ -612,7 +612,13 @@ def get_email_address(username): row = cur.fetchone() - username_email = row[0] + if row != None: + + username_email = row[0] + + else: + + username_email = None cur.close() @@ -636,6 +642,10 @@ def send_anotation(game_id): username_email = get_email_address(username) + if username_email == None: + + return (emailed, game_id, game_found) + # Create message object instance msg = MIMEMultipart() @@ -1042,12 +1052,13 @@ def load_strings1(bot_lang): game_no_exists = get_parameter("game_no_exists", language_filepath) it_not_exists = get_parameter("it_not_exists", language_filepath) game_already_started = get_parameter("game_already_started", language_filepath) + cant_send_to_fediverse_account = get_parameter("cant_send_to_fediverse_account", language_filepath) wait_other_player = get_parameter("wait_other_player", language_filepath) is_not_legal_move = get_parameter("is_not_legal_move", language_filepath) check_done = get_parameter("check_done", language_filepath) check_mate = get_parameter("check_mate", language_filepath) - return (game_number_anotations, anotations_sent, game_no_exists, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate) + return (game_number_anotations, anotations_sent, game_no_exists, cant_send_to_fediverse_account, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate) def load_strings2(bot_lang): @@ -1235,7 +1246,7 @@ if __name__ == '__main__': search_end, search_move, search_new, search_games, search_send, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error = load_strings(bot_lang) - game_number_anotations, anotations_sent, game_no_exists, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate = load_strings1(bot_lang) + game_number_anotations, anotations_sent, game_no_exists, cant_send_to_fediverse_account, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate = load_strings1(bot_lang) check_mate_movements, the_winner_is, well_done, winned_games, wins_of_many, lost_piece, not_legal_move_str, player_leave_game = load_strings2(bot_lang) @@ -1266,21 +1277,25 @@ if __name__ == '__main__': username, domain = get_user_domain(account_id) + if domain != None: + + username = username + '@' + domain + status_id = status_id_lst[i] replied = check_replies(status_id) - if replied == True or domain != None: + if replied == True: #or domain != None: i += 1 continue - if domain != None: + #if domain != None: - update_replies(username, status_id) + #update_replies(username, status_id) - i += 1 + #i += 1 # listen them or not @@ -1384,7 +1399,13 @@ if __name__ == '__main__': elif emailed == False and game_found == False: - toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists + if domain != None: + + toot_text = '@'+username + ' ' + cant_send_to_fediverse_account + + else: + + toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) -- 2.34.1 From 009580868bb9829c4bb8b9704bc58e5c9c4956b6 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 20:14:40 +0100 Subject: [PATCH 25/51] Added fediverse support! Any fediverse user can play Mastodon Chess! --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fb26fe..a2c0a38 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,5 @@ Within Python Virtual Environment: 23.11.2020 - New feature! Now all moves are saved to file (with san anotation). 25.11.2020 - New feature! Get any game anotations via email (see point 3 above). 27.11.2020 - New feature! Pawn promotion and locales support ( ca & eng ) -28.11.2020 - New feature! Added 'es' locale support +28.11.2020 - New feature! Added 'es' locale support +28.11.2020 - New feature! Now any fediverse user can play Mastodon Chess! -- 2.34.1 From 207635cd17646805c0a28361be77d151e207b197 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 28 Nov 2020 23:00:15 +0100 Subject: [PATCH 26/51] Added help --- README.md | 7 +++++- locales/ca.txt | 9 ++++++++ locales/en.txt | 9 ++++++++ locales/es.txt | 9 ++++++++ mastochess.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a2c0a38..20a9cbc 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Mastodon Chess (mastochess) uses [python-chess](https://python-chess.readthedocs ### How to play: +To get help: + +@your_bot_username help + To start a game: @your_bot_username new @@ -71,4 +75,5 @@ Within Python Virtual Environment: 25.11.2020 - New feature! Get any game anotations via email (see point 3 above). 27.11.2020 - New feature! Pawn promotion and locales support ( ca & eng ) 28.11.2020 - New feature! Added 'es' locale support -28.11.2020 - New feature! Now any fediverse user can play Mastodon Chess! +28.11.2020 - New feature! Now any fediverse user can play Mastodon Chess! +28.11.2020 - New feature! Added help diff --git a/locales/ca.txt b/locales/ca.txt index 439a2c4..0c1dae6 100644 --- a/locales/ca.txt +++ b/locales/ca.txt @@ -3,6 +3,7 @@ search_move: mou search_new: nova search_games: jocs search_send: envia +search_help: ajuda new_game_started: partida iniciada! Esperant jugador... playing_with: jugues amb your_turn: el teu torn @@ -47,3 +48,11 @@ rook_piece_letter: T queen_piece_letter: D king_piece_letter: R email_subject: Anotacions partida n. +start_or_join_a_new_game: nova (iniciar partida o unirse a una en espera) +move_a_piece: mou e2e3 (per exemple) +leave_a_game: fi (per a deixar la partida en qualsevol moment) +list_games: jocs (mostra un llistat de partides actives) +get_a_game_anotation: envia 1 (1 és el número de la partida. Envia les anotacions de la partida per correu electrònic, només usuaris del servidor local) +show_help: ajuda (mostra aquesta ajuda i, per tant, és l'ajuda de l'ajuda) + + diff --git a/locales/en.txt b/locales/en.txt index 6387db3..f94a778 100644 --- a/locales/en.txt +++ b/locales/en.txt @@ -3,6 +3,7 @@ search_move: move search_new: new search_games: games search_send: send +search_help: help new_game_started: game started! Waiting for the second player... playing_with: you play with your_turn: it's your turn @@ -47,3 +48,11 @@ rook_piece_letter: R queen_piece_letter: Q king_piece_letter: K email_subject: Anotations of game n. +start_or_join_a_new_game: new (start a new game or join a waiting one) +move_a_piece: move e2e3 (in ex.) +leave_a_game: end (to leave the game any time) +list_games: games (show an on going games list) +get_a_game_anotation: send 1 (1 is the game number. It send the game's anotations by email but local users only) +show_help: help (show this help so, it's the help of the help) + + diff --git a/locales/es.txt b/locales/es.txt index 6cbd5b1..0464870 100644 --- a/locales/es.txt +++ b/locales/es.txt @@ -3,6 +3,7 @@ search_move: mueve search_new: nueva search_games: partidas search_send: envia +search_help: ayuda new_game_started: partida iniciada! Esperando jugador... playing_with: juegas con your_turn: tu turno @@ -47,3 +48,11 @@ rook_piece_letter: T queen_piece_letter: D king_piece_letter: R email_subject: Anotaciones partida n. +start_or_join_a_new_game: nueva (empezar una partida o unirse a una en espera) +move_a_piece: mueve e2e3 (por ejemplo) +leave_a_game: fin (para dejar la partida en cualquier momento) +list_games: partidas (muestra un listado de partidas activas) +get_a_game_anotation: envia 1 (1 es el número de la partida. Envia las anotaciones de la partida pedida por correo electrónico pero sólo a usuarios del servidor local) +show_help: ayuda (muestra esta ayuda y, por tanto, es la ayuda de la ayuda) + + diff --git a/mastochess.py b/mastochess.py index 4f7b0cc..bb43bbd 100644 --- a/mastochess.py +++ b/mastochess.py @@ -167,7 +167,7 @@ def get_notification_data(): url_lst = [] - search_text = [search_end, search_move, search_new, search_games, search_send] + search_text = [search_end, search_move, search_new, search_games, search_send, search_help] conn = None @@ -964,6 +964,24 @@ def next_move(playing_user): conn.close() +def toot_help(): + + help_text = '@'+username + ' ' + search_help + ':\n' + help_text += '\n' + help_text += '@'+bot_username + ' ' + start_or_join_a_new_game + '\n' + help_text += '\n' + help_text += '@'+bot_username + ' ' + move_a_piece + '\n' + help_text += '\n' + help_text += '@'+bot_username + ' ' + leave_a_game + '\n' + help_text += '\n' + help_text += '@'+bot_username + ' ' + list_games + '\n' + help_text += '\n' + help_text += '@'+bot_username + ' ' + get_a_game_anotation + '\n' + help_text += '\n' + help_text += '@'+bot_username + ' ' + show_help + '\n' + + return help_text + def replying(): reply = False @@ -1019,6 +1037,10 @@ def replying(): reply = True + elif query_word == search_help: + + reply = True + else: reply = False @@ -1036,6 +1058,7 @@ def load_strings(bot_lang): search_new = get_parameter("search_new", language_filepath) search_games = get_parameter("search_games", language_filepath) search_send = get_parameter("search_send", language_filepath) + search_help = get_parameter("search_help", language_filepath) new_game_started = get_parameter("new_game_started", language_filepath) playing_with = get_parameter("playing_with", language_filepath) your_turn = get_parameter("your_turn", language_filepath) @@ -1043,7 +1066,7 @@ def load_strings(bot_lang): chess_hashtag = get_parameter("chess_hashtag", language_filepath) send_error = get_parameter("send_error", language_filepath) - return (search_end, search_move, search_new, search_games, search_send, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error) + return (search_end, search_move, search_new, search_games, search_send, search_help, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error) def load_strings1(bot_lang): @@ -1108,6 +1131,17 @@ def load_strings5(bot_lang): return (pawn_piece_letter, knight_piece_letter, bishop_piece_letter, rook_piece_letter, queen_piece_letter, king_piece_letter, email_subject) +def load_strings6(bot_lang): + + start_or_join_a_new_game = get_parameter("start_or_join_a_new_game", language_filepath) + move_a_piece = get_parameter("move_a_piece", language_filepath) + leave_a_game = get_parameter("leave_a_game", language_filepath) + list_games = get_parameter("list_games", language_filepath) + get_a_game_anotation = get_parameter("get_a_game_anotation", language_filepath) + show_help = get_parameter("show_help", language_filepath) + + return (start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help) + def mastodon(): # Load secrets from secrets file @@ -1244,7 +1278,7 @@ if __name__ == '__main__': search_send_slicing = 5 send_game_slicing = 6 - search_end, search_move, search_new, search_games, search_send, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error = load_strings(bot_lang) + search_end, search_move, search_new, search_games, search_send, search_help, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error = load_strings(bot_lang) game_number_anotations, anotations_sent, game_no_exists, cant_send_to_fediverse_account, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate = load_strings1(bot_lang) @@ -1256,6 +1290,8 @@ if __name__ == '__main__': pawn_piece_letter, knight_piece_letter, bishop_piece_letter, rook_piece_letter, queen_piece_letter, king_piece_letter, email_subject = load_strings5(bot_lang) + start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help = load_strings6(bot_lang) + mastodon, mastodon_hostname, bot_username = mastodon() mastodon_db, mastodon_db_user, chess_db, chess_db_user = db_config() @@ -1411,6 +1447,14 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word == search_help: + + help_text = toot_help() + + mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + else: update_replies(status_id, username, now) @@ -1746,6 +1790,14 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word == search_help: + + help_text = toot_help() + + mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + else: if playing_user == None: -- 2.34.1 From 3352f7de3c6443811958e2bf79b22db7df8bfbcb Mon Sep 17 00:00:00 2001 From: spla Date: Mon, 30 Nov 2020 09:50:08 +0100 Subject: [PATCH 27/51] Moved games, anotations and locales dirs to app/ --- app/anotations/0.txt | 2 ++ app/games/board.png | Bin 0 -> 29158 bytes app/locales/ca.txt | 58 +++++++++++++++++++++++++++++++++++++++++++ app/locales/en.txt | 58 +++++++++++++++++++++++++++++++++++++++++++ app/locales/es.txt | 58 +++++++++++++++++++++++++++++++++++++++++++ mastochess.py | 36 ++++++++------------------- 6 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 app/anotations/0.txt create mode 100644 app/games/board.png create mode 100644 app/locales/ca.txt create mode 100644 app/locales/en.txt create mode 100644 app/locales/es.txt diff --git a/app/anotations/0.txt b/app/anotations/0.txt new file mode 100644 index 0000000..0cd8a0f --- /dev/null +++ b/app/anotations/0.txt @@ -0,0 +1,2 @@ +game: 0 +player1 / player2 diff --git a/app/games/board.png b/app/games/board.png new file mode 100644 index 0000000000000000000000000000000000000000..314b85a0c753e62d3b7cdbb07bc471ffd8228544 GIT binary patch literal 29158 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 Date: Mon, 30 Nov 2020 09:52:16 +0100 Subject: [PATCH 28/51] Moved games, anotations and locales dirs to app/ --- locales/ca.txt | 58 -------------------------------------------------- locales/en.txt | 58 -------------------------------------------------- locales/es.txt | 58 -------------------------------------------------- 3 files changed, 174 deletions(-) delete mode 100644 locales/ca.txt delete mode 100644 locales/en.txt delete mode 100644 locales/es.txt diff --git a/locales/ca.txt b/locales/ca.txt deleted file mode 100644 index 0c1dae6..0000000 --- a/locales/ca.txt +++ /dev/null @@ -1,58 +0,0 @@ -search_end: fi -search_move: mou -search_new: nova -search_games: jocs -search_send: envia -search_help: ajuda -new_game_started: partida iniciada! Esperant jugador... -playing_with: jugues amb -your_turn: el teu torn -game_name: partida -chess_hashtag: #escacs -send_error: error al enviar les anotacions :-( -game_number_anotations: les anotacions de la partida n. -anotations_sent: enviades amb èxit! -game_no_exists: la partida n. -cant_send_to_fediverse_account: per ara no és possible :-( -it_not_exists: no existeix... -game_already_started: ja tenies iniciada una partida! -wait_other_player: espera l'altre jugador -is_not_legal_move: és un moviment il·legal. Torna a tirar. -check_done: t'ha fet escac! -check_mate: Escac i mat! (en -check_mate_movements: moviments) -the_winner_is: El guanyador és: -well_done: ben jugat! -winned_games: Partides guanyades: -wins_of_many: de -lost_piece: * has perdut -not_legal_move_str: moviment il·legal! -player_leave_game: ha deixat la partida amb -leave_waiting_game: has abandonat la partida en espera. -started_games: partides iniciades: -game_is_waiting: en espera... -game_is_on_going: (en joc) -no_on_going_games: cap partida en joc -is_not_your_turn: no és el teu torn. -is_the_turn_of: és el torn de -pawn_piece: un peó -knight_piece: un cavall -bishop_piece: l'alfil -rook_piece: una torre -queen_piece: la Dama -king_piece: el Rei -pawn_piece_letter: P -knight_piece_letter: C -bishop_piece_letter: A -rook_piece_letter: T -queen_piece_letter: D -king_piece_letter: R -email_subject: Anotacions partida n. -start_or_join_a_new_game: nova (iniciar partida o unirse a una en espera) -move_a_piece: mou e2e3 (per exemple) -leave_a_game: fi (per a deixar la partida en qualsevol moment) -list_games: jocs (mostra un llistat de partides actives) -get_a_game_anotation: envia 1 (1 és el número de la partida. Envia les anotacions de la partida per correu electrònic, només usuaris del servidor local) -show_help: ajuda (mostra aquesta ajuda i, per tant, és l'ajuda de l'ajuda) - - diff --git a/locales/en.txt b/locales/en.txt deleted file mode 100644 index f94a778..0000000 --- a/locales/en.txt +++ /dev/null @@ -1,58 +0,0 @@ -search_end: end -search_move: move -search_new: new -search_games: games -search_send: send -search_help: help -new_game_started: game started! Waiting for the second player... -playing_with: you play with -your_turn: it's your turn -game_name: game -chess_hashtag: #chess -send_error: sending anotations error :-( -game_number_anotations: the anotations of game n. -anotations_sent: succesfully sent! -game_no_exists: the game n. -cant_send_to_fediverse_account: not possible yet :-( -it_not_exists: don't exists... -game_already_started: you already started a game! -wait_other_player: wait for the second player -is_not_legal_move: is not a legal move. Play again. -check_done: you are in check! -check_mate: it's a checkmate! (in -check_mate_movements: moves) -the_winner_is: The winner is: -well_done: well done! -winned_games: Won games: -wins_of_many: of -lost_piece: * you have lost -not_legal_move_str: not a legal move! -player_leave_game: has left the game with -leave_waiting_game: you have left the game in hold. -started_games: started games: -game_is_waiting: on hold... -game_is_on_going: (on going) -no_on_going_games: no games -is_not_your_turn: it's not your turn. -is_the_turn_of: it's the turn of -pawn_piece: a pawn -knight_piece: one knight -bishop_piece: one bishop -rook_piece: a rook -queen_piece: the Queen -king_piece: the King -pawn_piece_letter: P -knight_piece_letter: N -bishop_piece_letter: B -rook_piece_letter: R -queen_piece_letter: Q -king_piece_letter: K -email_subject: Anotations of game n. -start_or_join_a_new_game: new (start a new game or join a waiting one) -move_a_piece: move e2e3 (in ex.) -leave_a_game: end (to leave the game any time) -list_games: games (show an on going games list) -get_a_game_anotation: send 1 (1 is the game number. It send the game's anotations by email but local users only) -show_help: help (show this help so, it's the help of the help) - - diff --git a/locales/es.txt b/locales/es.txt deleted file mode 100644 index 0464870..0000000 --- a/locales/es.txt +++ /dev/null @@ -1,58 +0,0 @@ -search_end: fin -search_move: mueve -search_new: nueva -search_games: partidas -search_send: envia -search_help: ayuda -new_game_started: partida iniciada! Esperando jugador... -playing_with: juegas con -your_turn: tu turno -game_name: partida -chess_hashtag: #ajedrez -send_error: error al enviar las anotaciones :-( -game_number_anotations: las anotaciones de la partida n. -anotations_sent: enviades con éxito! -game_no_exists: la partida n. -cant_send_to_fediverse_account: por ahora no es posible :-( -it_not_exists: no existe... -game_already_started: ja tenias iniciada una partida! -wait_other_player: espera al otro jugador -is_not_legal_move: es un movimiento ilegal. Vuelve a tirar. -check_done: te ha echo jaque! -check_mate: Jaque mate! (en -check_mate_movements: movimientos) -the_winner_is: El ganador es: -well_done: bien jugado! -winned_games: Partidas ganadas: -wins_of_many: de -lost_piece: * has perdido -not_legal_move_str: movimiento ilegal! -player_leave_game: ha dejado la partida con -leave_waiting_game: has abandonado la partida en espera. -started_games: partidas iniciadas: -game_is_waiting: en espera... -game_is_on_going: (en juego) -no_on_going_games: ninguna partida en juego -is_not_your_turn: no es tu turno. -is_the_turn_of: es el turno de -pawn_piece: un peón -knight_piece: un caballo -bishop_piece: el alfil -rook_piece: una torre -queen_piece: la Dama -king_piece: el Rey -pawn_piece_letter: P -knight_piece_letter: C -bishop_piece_letter: A -rook_piece_letter: T -queen_piece_letter: D -king_piece_letter: R -email_subject: Anotaciones partida n. -start_or_join_a_new_game: nueva (empezar una partida o unirse a una en espera) -move_a_piece: mueve e2e3 (por ejemplo) -leave_a_game: fin (para dejar la partida en cualquier momento) -list_games: partidas (muestra un listado de partidas activas) -get_a_game_anotation: envia 1 (1 es el número de la partida. Envia las anotaciones de la partida pedida por correo electrónico pero sólo a usuarios del servidor local) -show_help: ayuda (muestra esta ayuda y, por tanto, es la ayuda de la ayuda) - - -- 2.34.1 From b28fbaf764b6ef5c55bf15b9b770c15e68917d24 Mon Sep 17 00:00:00 2001 From: spla Date: Tue, 1 Dec 2020 15:28:26 +0100 Subject: [PATCH 29/51] Fix #8 and Fix #9 (added stalemate detection) --- app/locales/ca.txt | 2 +- app/locales/en.txt | 2 +- app/locales/es.txt | 2 +- mastochess.py | 92 ++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/app/locales/ca.txt b/app/locales/ca.txt index 0c1dae6..4903d2e 100644 --- a/app/locales/ca.txt +++ b/app/locales/ca.txt @@ -54,5 +54,5 @@ leave_a_game: fi (per a deixar la partida en qualsevol moment) list_games: jocs (mostra un llistat de partides actives) get_a_game_anotation: envia 1 (1 és el número de la partida. Envia les anotacions de la partida per correu electrònic, només usuaris del servidor local) show_help: ajuda (mostra aquesta ajuda i, per tant, és l'ajuda de l'ajuda) - +stalemate_str: taules! partida finalitzada. diff --git a/app/locales/en.txt b/app/locales/en.txt index f94a778..a6c6af7 100644 --- a/app/locales/en.txt +++ b/app/locales/en.txt @@ -54,5 +54,5 @@ leave_a_game: end (to leave the game any time) list_games: games (show an on going games list) get_a_game_anotation: send 1 (1 is the game number. It send the game's anotations by email but local users only) show_help: help (show this help so, it's the help of the help) - +stalemate_str: stalemate! game is over. diff --git a/app/locales/es.txt b/app/locales/es.txt index 0464870..008c827 100644 --- a/app/locales/es.txt +++ b/app/locales/es.txt @@ -54,5 +54,5 @@ leave_a_game: fin (para dejar la partida en cualquier momento) list_games: partidas (muestra un listado de partidas activas) get_a_game_anotation: envia 1 (1 es el número de la partida. Envia las anotaciones de la partida pedida por correo electrónico pero sólo a usuarios del servidor local) show_help: ayuda (muestra esta ayuda y, por tanto, es la ayuda de la ayuda) - +stalemate_str: Tablas! la partida ha terminado. diff --git a/mastochess.py b/mastochess.py index f5ac4d6..e66fce3 100644 --- a/mastochess.py +++ b/mastochess.py @@ -710,14 +710,64 @@ def send_anotation(game_id): pass return emailed -def close_game(): +def close_game(username): + + try: + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("select white_user, black_user from games where game_id=(%s)", (game_id,)) + + row = cur.fetchone() + + if row != None: + + white_player = row[0] + + black_player = row[1] + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() now = datetime.now() + winner = '' + waiting = False finished = True + if stalemate == True: + + winner = "stalemate" + + if black_user == '': + + winner = 'none' + + else: + + if query_word == search_end and username == white_user and stalemate == False: + + winner = black_user + + elif query_word == search_end and username == black_user and stalemate == False: + + winner = white_user + try: conn = None @@ -728,7 +778,7 @@ def close_game(): cur.execute("update games set waiting=(%s), finished=(%s), updated_at=(%s) where game_id=(%s)", (waiting, finished, now, game_id)) - cur.execute("update stats set winner=(%s), finished=(%s), updated_at=(%s) where game_id=(%s)", (username, finished, now, game_id)) + cur.execute("update stats set winner=(%s), finished=(%s), updated_at=(%s) where game_id=(%s)", (winner, finished, now, game_id)) conn.commit() @@ -1483,6 +1533,10 @@ if __name__ == '__main__': promoted = False + stalemate = False + + checkmate = False + try: piece_square_index = chess.SQUARE_NAMES.index(moving[:2]) @@ -1557,13 +1611,19 @@ if __name__ == '__main__': king_square = board.king(chess.WHITE) check = True + if board.is_stalemate() == True: + + stalemate = True + if board.is_game_over() == True: game_moves = board.ply() - close_game() + close_game(username) - checkmate = True + if stalemate == False: + + checkmate = True else: @@ -1589,6 +1649,22 @@ if __name__ == '__main__': toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + elif check == False and stalemate == True: + + toot_text = stalemate_str + ' (' + str(game_moves) + ')' + '\n' + + toot_text += '\n@'+playing_user + ', ' + '@'+username + "\n" + + toot_text += '\n' + winned_games + "\n" + + played_games, wins = get_stats(username) + + toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + + played_games, wins = get_stats(playing_user) + + toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + else: toot_text = '@'+playing_user + ' ' + your_turn + '\n' @@ -1667,6 +1743,8 @@ if __name__ == '__main__': elif query_word == search_end: + stalemate = False + if black_user != '': if username == white_user: @@ -1675,7 +1753,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - close_game() + close_game(username) update_replies(status_id, username, now) @@ -1689,7 +1767,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - close_game() + close_game(username) update_replies(status_id, username, now) @@ -1703,7 +1781,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) - close_game() + close_game(username) update_replies(status_id, username, now) -- 2.34.1 From 83cddfca9daf8f3a62e96f7ccbbafc240b4da150 Mon Sep 17 00:00:00 2001 From: spla Date: Wed, 2 Dec 2020 12:58:02 +0100 Subject: [PATCH 30/51] Added space between strings --- mastochess.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mastochess.py b/mastochess.py index e66fce3..79648c1 100644 --- a/mastochess.py +++ b/mastochess.py @@ -576,6 +576,7 @@ def save_anotation(moving): else: line_data = " - " + moved_piece_name + "=D" + else: line_data = " - " + moved_piece_name + "\n" @@ -1721,7 +1722,7 @@ if __name__ == '__main__': print(v_error) - toot_text = '@'+username + ' ' + not_legal_move_str + moving + '!?)\n' + toot_text = '@'+username + ' ' + not_legal_move_str + ' ' + moving + '!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1733,7 +1734,7 @@ if __name__ == '__main__': print(a_error) - toot_text = '@'+username + ' ' + not_legal_move_str + moving + '!?)\n' + toot_text = '@'+username + ' ' + not_legal_move_str + ' ' + moving + '!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) -- 2.34.1 From 1892141b5d03bb436f85f4454a41841326ecf4ef Mon Sep 17 00:00:00 2001 From: spla Date: Thu, 3 Dec 2020 13:33:40 +0100 Subject: [PATCH 31/51] New feature! Added pgn save & send support --- mastochess.py | 106 +++++++++++++++++++------------------------------- 1 file changed, 39 insertions(+), 67 deletions(-) diff --git a/mastochess.py b/mastochess.py index 79648c1..7de46b0 100644 --- a/mastochess.py +++ b/mastochess.py @@ -18,6 +18,7 @@ import psycopg2 import chess import chess.svg from cairosvg import svg2png +import chess.pgn def cleanhtml(raw_html): cleanr = re.compile('<.*?>') @@ -511,91 +512,59 @@ def update_game(board_game, toot_url): conn.close() -def save_anotation(moving): +def write_result(filename, old_string, new_string): - if moving_piece != 1: + with open(filename) as f: + s = f.read() + if old_string not in s: + print('"{old_string}" not found in {filename}.'.format(**locals())) + return - square_index = chess.SQUARE_NAMES.index(moving[2:]) + with open(filename, 'w') as f: + print('Changing "{old_string}" to "{new_string}" in {filename}'.format(**locals())) + s = s.replace(old_string, new_string) + f.write(s) - moved_piece = board.piece_type_at(square_index) +def save_anotation(moving, san_move): - moved_piece_name = get_moved_piece_name(moved_piece) - - else: - - moved_piece_name = 'P' - - if promoted == True: - - moving = moving + 'q' - - game_file = "app/anotations/" + str(game_id) + ".txt" - - if moved_piece_name == 'P': - - moved_piece_name = moved_piece_name.replace('P','') - - if capture == True: - - moved_piece_name = moved_piece_name + "X" - - if check == True: - - moved_piece_name = moved_piece_name + moving[2:] + "+" - - if checkmate == True: - - moved_piece_name = moved_piece_name + "+" + pgn_file = "app/anotations/" + str(game_id) + ".pgn" if bool(board.turn == chess.BLACK) == True: - if check != True and checkmate != True: - - if promoted != True: - - line_data = str(board.fullmove_number) + ". " + moved_piece_name + moving[2:] - - else: - - line_data = str(board.fullmove_number) + ". " + moved_piece_name + "=D" - - else: - - line_data = str(board.fullmove_number) + ". " + moved_piece_name + line_data = str(board.fullmove_number) + ". " + san_move else: - moved_piece_name = moved_piece_name.lower() + line_data = " " + san_move + " " - if check != True and checkmate != True: + if checkmate or stalemate: - if promoted != True: + line_data = line_data + " " + board.result() - line_data = " - " + moved_piece_name + moving[2:] + "\n" + write_result(pgn_file, '[Result ]', '[Result ' + board.result() + ' ]') - else: + if not os.path.isfile(pgn_file): - line_data = " - " + moved_piece_name + "=D" + file_header = '[Event ' + game_name + ': ' + str(game_id) + ']\n' + file_header += '[Site ' + mastodon_hostname + ']' + '\n' + file_header += '[Date ' + str(datetime.today().strftime('%d-%m-%Y')) + ']' + '\n' + file_header += '[Round ' + str(game_id) + ']' + '\n' + file_header += '[White ' + white_user + ' ]' + '\n' + file_header += '[Black ' + black_user + ' ]' + '\n' + file_header += '[Result ]' + '\n' + file_header += '[Time ' + str(datetime.now().strftime('%H:%M:%S')) + ']' + '\n\n' - else: - - line_data = " - " + moved_piece_name + "\n" - - if not os.path.isfile(game_file): - - file_header = game_name + ': ' + str(game_id) + "\n" + white_user + " / " + black_user + "\n\n" - - with open(game_file, 'w+') as f: + with open(pgn_file, 'w+') as f: f.write(file_header) - with open(game_file, 'a') as f: + with open(pgn_file, 'a') as f: f.write(line_data) else: - with open(game_file, 'a') as f: + with open(pgn_file, 'a') as f: f.write(line_data) @@ -656,7 +625,7 @@ def send_anotation(game_id): msg['Subject'] = email_subject + game_id # Attach the game anotation - file_to_attach = "app/anotations/" + game_id + ".txt" + file_to_attach = "app/anotations/" + game_id + ".pgn" try: attachment = open(file_to_attach, 'rb') @@ -697,19 +666,20 @@ def send_anotation(game_id): print(auth_error) pass - return emailed + return (emailed, game_id, game_found) except socket.gaierror as socket_error: print(socket_error) pass - return emailed + return (emailed, game_id, game_found) except SMTPRecipientsRefused as recip_error: print(recip_error) pass - return emailed + return (emailed, game_id, game_found) + def close_game(username): @@ -1464,7 +1434,7 @@ if __name__ == '__main__': if emailed == False and game_found == True: - toot_text = '@'+username + send_error + toot_text = '@'+username + ' ' + send_error elif emailed == True and game_found == True: @@ -1580,6 +1550,8 @@ if __name__ == '__main__': else: + san_move = board.san(chess.Move.from_uci(moving)) + check = False playing_user = next_move(username) @@ -1712,7 +1684,7 @@ if __name__ == '__main__': game_moves = board.ply() - save_anotation(moving) + save_anotation(moving, san_move) update_moves(username, game_moves) -- 2.34.1 From a5dc281cea4291674deb3672ef9adbfe0734927f Mon Sep 17 00:00:00 2001 From: spla Date: Thu, 3 Dec 2020 13:36:06 +0100 Subject: [PATCH 32/51] New feature! Added pgn save & send support --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 20a9cbc..8bc605d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To list on going games: @your_bot_username games -To get any game anotations, in ex. game 1: +To get any game anotations, in ex. game 1, in pgn format: @your_bot_username send 1 @@ -76,4 +76,5 @@ Within Python Virtual Environment: 27.11.2020 - New feature! Pawn promotion and locales support ( ca & eng ) 28.11.2020 - New feature! Added 'es' locale support 28.11.2020 - New feature! Now any fediverse user can play Mastodon Chess! -28.11.2020 - New feature! Added help +28.11.2020 - New feature! Added help +03.12.2020 - New feature! Added pgn save & send support -- 2.34.1 From 27bc472be587bf26ae6a7193f39edb8a64485320 Mon Sep 17 00:00:00 2001 From: spla Date: Thu, 3 Dec 2020 19:20:48 +0100 Subject: [PATCH 33/51] Fix issue #10 --- mastochess.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/mastochess.py b/mastochess.py index 7de46b0..fbaccaf 100644 --- a/mastochess.py +++ b/mastochess.py @@ -681,7 +681,7 @@ def send_anotation(game_id): return (emailed, game_id, game_found) -def close_game(username): +def close_game(username, checkmate): try: @@ -729,6 +729,10 @@ def close_game(username): winner = 'none' + if checkmate: + + winner = username + else: if query_word == search_end and username == white_user and stalemate == False: @@ -1592,12 +1596,12 @@ if __name__ == '__main__': game_moves = board.ply() - close_game(username) - if stalemate == False: checkmate = True + close_game(username, checkmate) + else: checkmate = False @@ -1718,6 +1722,8 @@ if __name__ == '__main__': stalemate = False + checkmate = False + if black_user != '': if username == white_user: @@ -1726,7 +1732,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - close_game(username) + close_game(username, checkmate) update_replies(status_id, username, now) @@ -1740,7 +1746,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - close_game(username) + close_game(username, checkmate) update_replies(status_id, username, now) @@ -1754,7 +1760,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) - close_game(username) + close_game(username, checkmate) update_replies(status_id, username, now) -- 2.34.1 From 0de5f2dcf36b3bc0459924c565daec2ee8a40d8b Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 4 Dec 2020 08:56:50 +0100 Subject: [PATCH 34/51] Added LICENSE --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2af6b79 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + nitter2toot + Copyright (C) 2020 spla + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + nitter2toot Copyright (C) 2020 spla + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. -- 2.34.1 From 40b16be125cad466e57dea6016f03ec7995b820b Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 4 Dec 2020 12:36:56 +0100 Subject: [PATCH 35/51] Removed unneeded function get_moved_piece_name() --- mastochess.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/mastochess.py b/mastochess.py index fbaccaf..688a7b7 100644 --- a/mastochess.py +++ b/mastochess.py @@ -126,34 +126,6 @@ def get_piece_name(captured_piece): return piece_name -def get_moved_piece_name(moved_piece): - - if moved_piece == 1: - - moved_piece_name = pawn_piece_letter - - if moved_piece == 2: - - moved_piece_name = knight_piece_letter - - if moved_piece == 3: - - moved_piece_name = bishop_piece_letter - - if moved_piece == 4: - - moved_piece_name = rook_piece_letter - - if moved_piece == 5: - - moved_piece_name = queen_piece_letter - - if moved_piece == 6: - - moved_piece_name = king_piece_letter - - return moved_piece_name - def get_notification_data(): try: -- 2.34.1 From 0fce3f1668333675ad0b4bebac699ab3ab041bc2 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 4 Dec 2020 19:47:05 +0100 Subject: [PATCH 36/51] New feature! players can claim a draw --- README.md | 23 +++++--- app/locales/ca.txt | 9 +++- app/locales/en.txt | 9 +++- app/locales/es.txt | 11 ++-- db-setup.py | 2 +- mastochess.py | 130 ++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 162 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8bc605d..e53fb93 100644 --- a/README.md +++ b/README.md @@ -4,31 +4,31 @@ Mastodon Chess (mastochess) uses [python-chess](https://python-chess.readthedocs ### How to play: -To get help: +- To get help: @your_bot_username help -To start a game: +- To start a game: @your_bot_username new -To make a move: +- To make a move: @your_bot_username move e2e4 -To finish game at any time: +- To finish game at any time: @your_bot_username end -To list on going games: +- To list on going games: @your_bot_username games -To get any game anotations, in ex. game 1, in pgn format: +- To get any game anotations, in ex. game 1, in pgn format: @your_bot_username send 1 -To promote a pawn use first letter of desired piece: +- To promote a pawn use first letter of desired piece: @your_bot_username move g7g8r (if you want a rook) @@ -38,6 +38,12 @@ r = rook Don't use q for queen. Pawn is promoted to Queen by default. +- To claim a draw: + +@your_bot_username draw + +### Commands table + | ca | en | es | ex. | Observ. | |:-----:|:-----:|:--------:|:----:|:-----------:| | nova | new | nueva | | | @@ -45,6 +51,8 @@ Don't use q for queen. Pawn is promoted to Queen by default. | fi | end | fin | | | | jocs | games | partidas | | | | envia | send | envia | 1 | game number | +| taules| draw | tablas | | | +| ajuda | help | ayuda | | | ### Dependencies @@ -78,3 +86,4 @@ Within Python Virtual Environment: 28.11.2020 - New feature! Now any fediverse user can play Mastodon Chess! 28.11.2020 - New feature! Added help 03.12.2020 - New feature! Added pgn save & send support +04.12.2020 - New feature! Now players can claim a draw. diff --git a/app/locales/ca.txt b/app/locales/ca.txt index 4903d2e..5132615 100644 --- a/app/locales/ca.txt +++ b/app/locales/ca.txt @@ -4,6 +4,7 @@ search_new: nova search_games: jocs search_send: envia search_help: ajuda +search_draw: taules new_game_started: partida iniciada! Esperant jugador... playing_with: jugues amb your_turn: el teu torn @@ -52,7 +53,11 @@ start_or_join_a_new_game: nova (iniciar partida o unirse a una en espera) move_a_piece: mou e2e3 (per exemple) leave_a_game: fi (per a deixar la partida en qualsevol moment) list_games: jocs (mostra un llistat de partides actives) -get_a_game_anotation: envia 1 (1 és el número de la partida. Envia les anotacions de la partida per correu electrònic, només usuaris del servidor local) +get_a_game_anotation: envia 1 (1 és el número de la partida. Envia les anotacions per correu electrònic, en format pgn. Només usuaris locals.) show_help: ajuda (mostra aquesta ajuda i, per tant, és l'ajuda de l'ajuda) stalemate_str: taules! partida finalitzada. - +ask_for_draw: taules +claim_draw_str: ha proposat taules a +draw_and_str: i +agreed_draw_str: han acordat taules. +claim_a_draw: taules (per a proposar/acceptar taules) diff --git a/app/locales/en.txt b/app/locales/en.txt index a6c6af7..307835c 100644 --- a/app/locales/en.txt +++ b/app/locales/en.txt @@ -4,6 +4,7 @@ search_new: new search_games: games search_send: send search_help: help +search_draw: draw new_game_started: game started! Waiting for the second player... playing_with: you play with your_turn: it's your turn @@ -52,7 +53,11 @@ start_or_join_a_new_game: new (start a new game or join a waiting one) move_a_piece: move e2e3 (in ex.) leave_a_game: end (to leave the game any time) list_games: games (show an on going games list) -get_a_game_anotation: send 1 (1 is the game number. It send the game's anotations by email but local users only) +get_a_game_anotation: send 1 (1 is the game number. It send the game's anotations by email, pgn format. Local users only.) show_help: help (show this help so, it's the help of the help) stalemate_str: stalemate! game is over. - +ask_for_draw: draw +claim_draw_str: had claimed draw to +draw_and_str: and +agreed_draw_str: agreed draw. +claim_a_draw: draw (to claim/accept a draw) diff --git a/app/locales/es.txt b/app/locales/es.txt index 008c827..2d00b01 100644 --- a/app/locales/es.txt +++ b/app/locales/es.txt @@ -4,6 +4,7 @@ search_new: nueva search_games: partidas search_send: envia search_help: ayuda +search_draw: tablas new_game_started: partida iniciada! Esperando jugador... playing_with: juegas con your_turn: tu turno @@ -50,9 +51,13 @@ king_piece_letter: R email_subject: Anotaciones partida n. start_or_join_a_new_game: nueva (empezar una partida o unirse a una en espera) move_a_piece: mueve e2e3 (por ejemplo) -leave_a_game: fin (para dejar la partida en cualquier momento) +leave_a_game: fin (dejar la partida en cualquier momento) list_games: partidas (muestra un listado de partidas activas) -get_a_game_anotation: envia 1 (1 es el número de la partida. Envia las anotaciones de la partida pedida por correo electrónico pero sólo a usuarios del servidor local) +get_a_game_anotation: envia 1 (1 es el número de la partida. Envia las anotaciones por correo electrónico, en formato pgn. Sólo usuarios locales.) show_help: ayuda (muestra esta ayuda y, por tanto, es la ayuda de la ayuda) stalemate_str: Tablas! la partida ha terminado. - +ask_for_draw: tablas +claim_draw_str: ha propuesto tablas a +draw_and_str: y +agreed_draw_str: han acordado tablas. +claim_a_draw: tablas (para proponer/aceptar tablas) diff --git a/db-setup.py b/db-setup.py index f04524a..14a42d4 100644 --- a/db-setup.py +++ b/db-setup.py @@ -160,7 +160,7 @@ if __name__ == '__main__': table = "games" sql = "create table "+table+" (created_at timestamptz, game_id serial, white_user varchar(40), black_user varchar(40), chess_game varchar(200), " sql +=" chess_status varchar(12), waiting boolean, updated_at timestamptz, next_move varchar(40), last_move varchar(40), moves int, finished boolean default False, " - sql += "chess_link varchar(100), PRIMARY KEY(game_id))" + sql += "chess_link varchar(100), white_stalemate boolean default False, black_stalemate boolean default False, PRIMARY KEY(game_id))" create_table(db, db_user, table, sql) table = "stats" diff --git a/mastochess.py b/mastochess.py index 688a7b7..e8047f8 100644 --- a/mastochess.py +++ b/mastochess.py @@ -128,6 +128,8 @@ def get_piece_name(captured_piece): def get_notification_data(): + conn = None + try: account_id_lst = [] @@ -140,9 +142,7 @@ def get_notification_data(): url_lst = [] - search_text = [search_end, search_move, search_new, search_games, search_send, search_help] - - conn = None + search_text = [search_end, search_move, search_new, search_games, search_send, search_help, search_draw] conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") @@ -653,6 +653,73 @@ def send_anotation(game_id): return (emailed, game_id, game_found) +def claim_draw(username): + + try: + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("select white_user, black_user from games where game_id=(%s)", (game_id,)) + + row = cur.fetchone() + + if row != None: + + white_player = row[0] + + black_player = row[1] + + if white_player == username: + + toot_text = '@'+username + ' ' + claim_draw_str + ' @'+black_player + '\n' + + cur.execute("update games set white_stalemate = 't' where game_id=(%s)", (game_id,)) + + else: + + toot_text = '@'+username + ' ha proposat taules a ' + '@'+white_player + '\n' + + cur.execute("update games set black_stalemate = 't' where game_id=(%s)", (game_id,)) + + conn.commit() + + cur.execute("select white_stalemate, black_stalemate from games where game_id=(%s)", (game_id,)) + + row = cur.fetchone() + + if row != None: + + white_stalemate = row[0] + + black_stalemate = row[1] + + cur.close() + + if white_stalemate == True and black_stalemate == True: + + stalemate = True + + else: + + stalemate = False + + return (white_player, black_player, toot_text, stalemate) + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + + def close_game(username, checkmate): try: @@ -963,7 +1030,7 @@ def next_move(playing_user): def toot_help(): - help_text = '@'+username + ' ' + search_help + ':\n' + help_text = '@'+username + '\n' help_text += '\n' help_text += '@'+bot_username + ' ' + start_or_join_a_new_game + '\n' help_text += '\n' @@ -975,7 +1042,7 @@ def toot_help(): help_text += '\n' help_text += '@'+bot_username + ' ' + get_a_game_anotation + '\n' help_text += '\n' - help_text += '@'+bot_username + ' ' + show_help + '\n' + help_text += '@'+bot_username + ' ' + claim_a_draw + '\n' return help_text @@ -1038,6 +1105,10 @@ def replying(): reply = True + elif query_word == search_draw: + + reply = True + else: reply = False @@ -1136,8 +1207,19 @@ def load_strings6(bot_lang): list_games = get_parameter("list_games", language_filepath) get_a_game_anotation = get_parameter("get_a_game_anotation", language_filepath) show_help = get_parameter("show_help", language_filepath) + search_draw = get_parameter("search_draw", language_filepath) + ask_for_draw = get_parameter("ask_for_draw", language_filepath) - return (start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help) + return (start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help, search_draw, ask_for_draw) + +def load_strings7(bot_lang): + + claim_draw_str = get_parameter("claim_draw_str", language_filepath) + draw_and_str = get_parameter("draw_and_str", language_filepath) + agreed_draw_str = get_parameter("agreed_draw_str", language_filepath) + claim_a_draw = get_parameter("claim_a_draw", language_filepath) + + return (claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw) def mastodon(): @@ -1281,7 +1363,9 @@ if __name__ == '__main__': pawn_piece_letter, knight_piece_letter, bishop_piece_letter, rook_piece_letter, queen_piece_letter, king_piece_letter, email_subject = load_strings5(bot_lang) - start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help = load_strings6(bot_lang) + start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help, search_draw, ask_for_draw = load_strings6(bot_lang) + + claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw = load_strings7(bot_lang) mastodon, mastodon_hostname, bot_username = mastodon() @@ -1434,6 +1518,8 @@ if __name__ == '__main__': help_text = toot_help() + help_text = (help_text[:490] + '... ') if len(help_text) > 490 else help_text + mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility) update_replies(status_id, username, now) @@ -1805,6 +1891,36 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word == search_draw: + + white_player, black_player, toot_text, stalemate = claim_draw(username) + + if stalemate == True: + + checkmate = False + + close_game(username, checkmate) + + toot_text = '@'+white_player + ' ' + draw_and_str + ' ' + '@'+black_player + ' ' + agreed_draw_str + '\n\n' + + toot_text += '\n' + winned_games + "\n" + + played_games, wins = get_stats(white_player) + + toot_text += white_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + + played_games, wins = get_stats(black_player) + + toot_text += black_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + else: + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + elif query_word == search_help: help_text = toot_help() -- 2.34.1 From 3e539de24ad7ea027d758645d6d8dcfac05241e8 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 4 Dec 2020 19:49:40 +0100 Subject: [PATCH 37/51] New feature! players can claim a draw --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e53fb93..22d5cc2 100644 --- a/README.md +++ b/README.md @@ -85,5 +85,5 @@ Within Python Virtual Environment: 28.11.2020 - New feature! Added 'es' locale support 28.11.2020 - New feature! Now any fediverse user can play Mastodon Chess! 28.11.2020 - New feature! Added help -03.12.2020 - New feature! Added pgn save & send support +03.12.2020 - New feature! Added pgn save & send support 04.12.2020 - New feature! Now players can claim a draw. -- 2.34.1 From 02f2ae6e69116e6864c1c3c838208da427d28db1 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 5 Dec 2020 13:41:53 +0100 Subject: [PATCH 38/51] New feature! Added panel stats --- README.md | 8 +++- app/fonts/DroidSans.ttf | Bin 0 -> 190776 bytes app/locales/ca.txt | 5 +++ app/locales/en.txt | 5 +++ app/locales/es.txt | 5 +++ app/panel/chess.png | Bin 0 -> 42371 bytes app/panel/fons.jpg | Bin 0 -> 12797 bytes app/panel/logo.png | Bin 0 -> 3693 bytes mastochess.py | 89 ++++++++++++++++++++++++++++++++++++++-- 9 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 app/fonts/DroidSans.ttf create mode 100644 app/panel/chess.png create mode 100644 app/panel/fons.jpg create mode 100644 app/panel/logo.png diff --git a/README.md b/README.md index 22d5cc2..f514eda 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ Don't use q for queen. Pawn is promoted to Queen by default. @your_bot_username draw +- To get your panel stats: + +@your_bot_username panel + ### Commands table | ca | en | es | ex. | Observ. | @@ -53,6 +57,7 @@ Don't use q for queen. Pawn is promoted to Queen by default. | envia | send | envia | 1 | game number | | taules| draw | tablas | | | | ajuda | help | ayuda | | | +| panell| panel | panel | | | ### Dependencies @@ -86,4 +91,5 @@ Within Python Virtual Environment: 28.11.2020 - New feature! Now any fediverse user can play Mastodon Chess! 28.11.2020 - New feature! Added help 03.12.2020 - New feature! Added pgn save & send support -04.12.2020 - New feature! Now players can claim a draw. +04.12.2020 - New feature! Now players can claim a draw. +05.12.2020 - New feature! Add panel stats. diff --git a/app/fonts/DroidSans.ttf b/app/fonts/DroidSans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ad1efca88aed8d9e2d179f27dd713e2a1562fe5f GIT binary patch literal 190776 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 literal 0 HcmV?d00001 diff --git a/app/locales/ca.txt b/app/locales/ca.txt index 5132615..d96d1a3 100644 --- a/app/locales/ca.txt +++ b/app/locales/ca.txt @@ -61,3 +61,8 @@ claim_draw_str: ha proposat taules a draw_and_str: i agreed_draw_str: han acordat taules. claim_a_draw: taules (per a proposar/acceptar taules) +search_panel: panell +panel_title_str: Panell de +panel_games_str: Partides +panel_wins_str: Victòries +panel_ratio_str: Ràtio diff --git a/app/locales/en.txt b/app/locales/en.txt index 307835c..0d71b5f 100644 --- a/app/locales/en.txt +++ b/app/locales/en.txt @@ -61,3 +61,8 @@ claim_draw_str: had claimed draw to draw_and_str: and agreed_draw_str: agreed draw. claim_a_draw: draw (to claim/accept a draw) +search_panel: panel +panel_title_str: Panel of +panel_games_str: Games +panel_wins_str: Wins +panel_ratio_str: Ratio diff --git a/app/locales/es.txt b/app/locales/es.txt index 2d00b01..320256f 100644 --- a/app/locales/es.txt +++ b/app/locales/es.txt @@ -61,3 +61,8 @@ claim_draw_str: ha propuesto tablas a draw_and_str: y agreed_draw_str: han acordado tablas. claim_a_draw: tablas (para proponer/aceptar tablas) +search_panel: panel +panel_title_str: Panel de +panel_games_str: Partidas +panel_wins_str: Victorias +panel_ratio_str: Ratio diff --git a/app/panel/chess.png b/app/panel/chess.png new file mode 100644 index 0000000000000000000000000000000000000000..c9a398e86de328d21d3fc7be7dedbc68d58cbedc GIT binary patch literal 42371 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%0DSx#c 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

Yv9U+s}tb?7eLq-oC;EcT$Y+$Vn?=Kx&hEO5SO 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�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 vi-=1|NeUy>$q7D#Cba+2lH(^IWewu$~PrhwI` literal 0 HcmV?d00001 diff --git a/app/panel/fons.jpg b/app/panel/fons.jpg new file mode 100644 index 0000000000000000000000000000000000000000..921d58664aff7ca992ab7ef2ee4d981d89265582 GIT binary patch literal 12797 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

$k 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 SZNyuyh?w{NLvyT?#s3LTFPQ8A literal 0 HcmV?d00001 diff --git a/app/panel/logo.png b/app/panel/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..244bd3bb8b1e650e47e74e2d54e6e0a40df24ce5 GIT binary patch literal 3693 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') @@ -29,6 +30,47 @@ def unescape(s): s = s.replace("'", "'") return s +def create_panel(username, played_games, wins): + + ratio = round((wins * 100) / played_games, 2) + x = 10 + y = 10 + + fons = Image.open('app/panel/fons.jpg') + print(fons.size) + + # add chess icon + icon_path = 'app/panel/chess.png' + icon_img = Image.open(icon_path) + + fons.paste(icon_img, (y+350, x+50), icon_img) + + logo_img = Image.open('app/panel/logo.png') + fons.paste(logo_img, (15, 320), logo_img) + + fons.save('app/panel/panel.png',"PNG") + + base = Image.open('app/panel/panel.png').convert('RGBA') + txt = Image.new('RGBA', base.size, (255,255,255,0)) + fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 40, layout_engine=ImageFont.LAYOUT_BASIC) + # get a drawing context + draw = ImageDraw.Draw(txt) + + draw.text((y+200,x+20), panel_title_str + ' ' + username, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + + fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 25, layout_engine=ImageFont.LAYOUT_BASIC) + + draw.text((y+70,x+120), panel_games_str + ': ' + str(played_games), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+70,x+170), panel_wins_str + ': ' + str(wins), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+70,x+220), panel_ratio_str + ': ' + str(ratio) + '%', font=fnt, fill=(255,255,255,220)) + + fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 15, layout_engine=ImageFont.LAYOUT_BASIC) + + draw.text((60,330), bot_username + '@' + mastodon_hostname + ' - 2020', font=fnt, fill=(255,255,255,200)) #fill=(255,255,255,255)) ## full opacity + + out = Image.alpha_composite(base, txt) + out.save('app/panel/' + username + '_panel.png') + def get_bot_id(): ################################################################################################################################### @@ -142,7 +184,7 @@ def get_notification_data(): url_lst = [] - search_text = [search_end, search_move, search_new, search_games, search_send, search_help, search_draw] + search_text = [search_end, search_move, search_new, search_games, search_send, search_help, search_draw, search_panel] conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") @@ -1109,6 +1151,10 @@ def replying(): reply = True + elif query_word == search_panel: + + reply = True + else: reply = False @@ -1218,8 +1264,13 @@ def load_strings7(bot_lang): draw_and_str = get_parameter("draw_and_str", language_filepath) agreed_draw_str = get_parameter("agreed_draw_str", language_filepath) claim_a_draw = get_parameter("claim_a_draw", language_filepath) + search_panel = get_parameter("search_panel", language_filepath) + panel_title_str = get_parameter("panel_title_str", language_filepath) + panel_games_str = get_parameter("panel_games_str", language_filepath) + panel_wins_str = get_parameter("panel_wins_str", language_filepath) + panel_ratio_str = get_parameter("panel_ratio_str", language_filepath) - return (claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw) + return (claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str) def mastodon(): @@ -1365,7 +1416,7 @@ if __name__ == '__main__': start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help, search_draw, ask_for_draw = load_strings6(bot_lang) - claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw = load_strings7(bot_lang) + claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str = load_strings7(bot_lang) mastodon, mastodon_hostname, bot_username = mastodon() @@ -1514,6 +1565,22 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word == search_panel: + + played_games, wins = get_stats(username) + + create_panel(username, played_games, wins) + + toot_text = '@'+username + + saved_panel = 'app/panel/' + username + '_panel.png' + + image_id = mastodon.media_post(saved_panel, "image/png").id + + mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility, media_ids={image_id}) + + update_replies(status_id, username, now) + elif query_word == search_help: help_text = toot_help() @@ -1921,6 +1988,22 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word == search_panel: + + played_games, wins = get_stats(username) + + create_panel(username, played_games, wins) + + toot_text = '@'+username + + saved_panel = 'app/panel/' + username + '_panel.png' + + image_id = mastodon.media_post(saved_panel, "image/png").id + + mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility, media_ids={image_id}) + + update_replies(status_id, username, now) + elif query_word == search_help: help_text = toot_help() -- 2.34.1 From 6641fad072fda316f5d35059317f48db34818630 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 5 Dec 2020 16:06:55 +0100 Subject: [PATCH 39/51] Fix #11. Count games with moves > 0 only --- mastochess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mastochess.py b/mastochess.py index e2cb6c7..0809dd2 100644 --- a/mastochess.py +++ b/mastochess.py @@ -864,7 +864,7 @@ def get_stats(player): cur = conn.cursor() - cur.execute("select count(*) from stats where white_user = (%s) or black_user = (%s) and finished", (player, player)) + cur.execute("select count(*) from games where (white_user = (%s) or black_user = (%s)) and finished and moves > 0", (player, player)) row = cur.fetchone() -- 2.34.1 From 77851ded7d4499c8fdd5adfe62610ed1713cedb0 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 5 Dec 2020 16:31:26 +0100 Subject: [PATCH 40/51] Fix #12 --- mastochess.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mastochess.py b/mastochess.py index 0809dd2..db79da0 100644 --- a/mastochess.py +++ b/mastochess.py @@ -32,7 +32,14 @@ def unescape(s): def create_panel(username, played_games, wins): - ratio = round((wins * 100) / played_games, 2) + if played_games > 0 and wins > 0: + + ratio = round((wins * 100) / played_games, 2) + + else: + + ratio = 0 + x = 10 y = 10 -- 2.34.1 From 72dfa742d793ff298a77762c2cd44d20aff72cd2 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 5 Dec 2020 17:36:04 +0100 Subject: [PATCH 41/51] Changed help from text to graphics --- app/locales/ca.txt | 3 +- app/locales/en.txt | 3 +- app/locales/es.txt | 3 +- mastochess.py | 79 ++++++++++++++++++++++++++++++++++------------ 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/app/locales/ca.txt b/app/locales/ca.txt index d96d1a3..e4862ca 100644 --- a/app/locales/ca.txt +++ b/app/locales/ca.txt @@ -53,7 +53,7 @@ start_or_join_a_new_game: nova (iniciar partida o unirse a una en espera) move_a_piece: mou e2e3 (per exemple) leave_a_game: fi (per a deixar la partida en qualsevol moment) list_games: jocs (mostra un llistat de partides actives) -get_a_game_anotation: envia 1 (1 és el número de la partida. Envia les anotacions per correu electrònic, en format pgn. Només usuaris locals.) +get_a_game_anotation: envia 1 (envia la partida en format pgn.) show_help: ajuda (mostra aquesta ajuda i, per tant, és l'ajuda de l'ajuda) stalemate_str: taules! partida finalitzada. ask_for_draw: taules @@ -66,3 +66,4 @@ panel_title_str: Panell de panel_games_str: Partides panel_wins_str: Victòries panel_ratio_str: Ràtio +post_my_panel_str: panell (publica les estadístiques) diff --git a/app/locales/en.txt b/app/locales/en.txt index 0d71b5f..56532ea 100644 --- a/app/locales/en.txt +++ b/app/locales/en.txt @@ -53,7 +53,7 @@ start_or_join_a_new_game: new (start a new game or join a waiting one) move_a_piece: move e2e3 (in ex.) leave_a_game: end (to leave the game any time) list_games: games (show an on going games list) -get_a_game_anotation: send 1 (1 is the game number. It send the game's anotations by email, pgn format. Local users only.) +get_a_game_anotation: send 1 (send the game in pgn format.) show_help: help (show this help so, it's the help of the help) stalemate_str: stalemate! game is over. ask_for_draw: draw @@ -66,3 +66,4 @@ panel_title_str: Panel of panel_games_str: Games panel_wins_str: Wins panel_ratio_str: Ratio +post_my_panel_str: panel (post player stats) diff --git a/app/locales/es.txt b/app/locales/es.txt index 320256f..7341159 100644 --- a/app/locales/es.txt +++ b/app/locales/es.txt @@ -53,7 +53,7 @@ start_or_join_a_new_game: nueva (empezar una partida o unirse a una en espera) move_a_piece: mueve e2e3 (por ejemplo) leave_a_game: fin (dejar la partida en cualquier momento) list_games: partidas (muestra un listado de partidas activas) -get_a_game_anotation: envia 1 (1 es el número de la partida. Envia las anotaciones por correo electrónico, en formato pgn. Sólo usuarios locales.) +get_a_game_anotation: envia 1 (envia la partida en formato pgn.) show_help: ayuda (muestra esta ayuda y, por tanto, es la ayuda de la ayuda) stalemate_str: Tablas! la partida ha terminado. ask_for_draw: tablas @@ -66,3 +66,4 @@ panel_title_str: Panel de panel_games_str: Partidas panel_wins_str: Victorias panel_ratio_str: Ratio +post_my_panel_str: panel (publica el panel de datos) diff --git a/mastochess.py b/mastochess.py index db79da0..26fa061 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1079,21 +1079,47 @@ def next_move(playing_user): def toot_help(): - help_text = '@'+username + '\n' - help_text += '\n' - help_text += '@'+bot_username + ' ' + start_or_join_a_new_game + '\n' - help_text += '\n' - help_text += '@'+bot_username + ' ' + move_a_piece + '\n' - help_text += '\n' - help_text += '@'+bot_username + ' ' + leave_a_game + '\n' - help_text += '\n' - help_text += '@'+bot_username + ' ' + list_games + '\n' - help_text += '\n' - help_text += '@'+bot_username + ' ' + get_a_game_anotation + '\n' - help_text += '\n' - help_text += '@'+bot_username + ' ' + claim_a_draw + '\n' + x = 10 + y = 10 - return help_text + fons = Image.open('app/panel/fons.jpg') + print(fons.size) + + # add chess icon + #icon_path = 'app/panel/chess.png' + #icon_img = Image.open(icon_path) + + #fons.paste(icon_img, (y+350, x+50), icon_img) + + logo_img = Image.open('app/panel/logo.png') + fons.paste(logo_img, (15, 320), logo_img) + + fons.save('app/panel/panel.png',"PNG") + + base = Image.open('app/panel/panel.png').convert('RGBA') + txt = Image.new('RGBA', base.size, (255,255,255,0)) + fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 40, layout_engine=ImageFont.LAYOUT_BASIC) + # get a drawing context + draw = ImageDraw.Draw(txt) + + draw.text((y+270,x+20), search_help, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + + fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 18, layout_engine=ImageFont.LAYOUT_BASIC) + + draw.text((y+80,x+80), '@'+bot_username + ' ' + start_or_join_a_new_game , font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+80,x+110), '@'+bot_username + ' ' + move_a_piece, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+80,x+140), '@'+bot_username + ' ' + leave_a_game, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+170), '@'+bot_username + ' ' + list_games, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+200), '@'+bot_username + ' ' + get_a_game_anotation, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+230), '@'+bot_username + ' ' + claim_a_draw, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+260), '@'+bot_username + ' ' + post_my_panel, font=fnt, fill=(255,255,255,220)) + + fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 15, layout_engine=ImageFont.LAYOUT_BASIC) + + draw.text((60,330), bot_username + '@' + mastodon_hostname + ' - 2020', font=fnt, fill=(255,255,255,200)) #fill=(255,255,255,255)) ## full opacity + + out = Image.alpha_composite(base, txt) + out.save('app/panel/help_panel.png') def replying(): @@ -1276,8 +1302,9 @@ def load_strings7(bot_lang): panel_games_str = get_parameter("panel_games_str", language_filepath) panel_wins_str = get_parameter("panel_wins_str", language_filepath) panel_ratio_str = get_parameter("panel_ratio_str", language_filepath) + post_my_panel_str = get_parameter("post_my_panel_str", language_filepath) - return (claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str) + return (claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str, post_my_panel_str) def mastodon(): @@ -1423,7 +1450,7 @@ if __name__ == '__main__': start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help, search_draw, ask_for_draw = load_strings6(bot_lang) - claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str = load_strings7(bot_lang) + claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str, post_my_panel = load_strings7(bot_lang) mastodon, mastodon_hostname, bot_username = mastodon() @@ -1590,11 +1617,15 @@ if __name__ == '__main__': elif query_word == search_help: - help_text = toot_help() + toot_help() - help_text = (help_text[:490] + '... ') if len(help_text) > 490 else help_text + help_text = '@'+username - mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility) + help_panel = 'app/panel/help_panel.png' + + image_id = mastodon.media_post(help_panel, "image/png").id + + mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) update_replies(status_id, username, now) @@ -2013,9 +2044,15 @@ if __name__ == '__main__': elif query_word == search_help: - help_text = toot_help() + toot_help() - mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility) + help_text = '@'+username + + help_panel = 'app/panel/help_panel.png' + + image_id = mastodon.media_post(help_panel, "image/png").id + + mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) update_replies(status_id, username, now) -- 2.34.1 From 8cce92eae83ac6f989ad3f2596f2eb648af67b48 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 5 Dec 2020 17:58:32 +0100 Subject: [PATCH 42/51] Changed panel layout --- mastochess.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mastochess.py b/mastochess.py index 26fa061..4ba7093 100644 --- a/mastochess.py +++ b/mastochess.py @@ -46,11 +46,15 @@ def create_panel(username, played_games, wins): fons = Image.open('app/panel/fons.jpg') print(fons.size) + large, high = fons.size + + title_length = len(panel_title_str + ' ' + username) + # add chess icon icon_path = 'app/panel/chess.png' icon_img = Image.open(icon_path) - fons.paste(icon_img, (y+350, x+50), icon_img) + fons.paste(icon_img, (y+300, x+50), icon_img) logo_img = Image.open('app/panel/logo.png') fons.paste(logo_img, (15, 320), logo_img) @@ -63,13 +67,13 @@ def create_panel(username, played_games, wins): # get a drawing context draw = ImageDraw.Draw(txt) - draw.text((y+200,x+20), panel_title_str + ' ' + username, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((((large / 2) - (title_length * 2)),x+20), panel_title_str + ' ' + username, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity - fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 25, layout_engine=ImageFont.LAYOUT_BASIC) + fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 35, layout_engine=ImageFont.LAYOUT_BASIC) draw.text((y+70,x+120), panel_games_str + ': ' + str(played_games), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity draw.text((y+70,x+170), panel_wins_str + ': ' + str(wins), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity - draw.text((y+70,x+220), panel_ratio_str + ': ' + str(ratio) + '%', font=fnt, fill=(255,255,255,220)) + draw.text((y+70,x+220), panel_ratio_str + ': ' + str(ratio) + '%', font=fnt, fill=(255,255,255,220)) fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 15, layout_engine=ImageFont.LAYOUT_BASIC) -- 2.34.1 From b19e1b7a5a9857ad78fad06e09db3ba6c7afe691 Mon Sep 17 00:00:00 2001 From: retiolus Date: Mon, 14 Dec 2020 18:56:17 +0000 Subject: [PATCH 43/51] Add new French translation --- app/locales/fr.txt | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 app/locales/fr.txt diff --git a/app/locales/fr.txt b/app/locales/fr.txt new file mode 100644 index 0000000..cde460e --- /dev/null +++ b/app/locales/fr.txt @@ -0,0 +1,69 @@ +search_end: fin +search_move: déplace +search_new: nouvelle +search_games: parties +search_send: envoyer +search_help: aide +search_draw: nulle +new_game_started: partie initié! En attente d'un joueur... +playing_with: vous jouez avec +your_turn: votre tour +game_name: partie +chess_hashtag: #échecs +send_error: erreur à l'envoyer les annotations :-( +game_number_anotations: es annotations de la partie n. +anotations_sent: envoyés avec succès ! +game_no_exists: la partie n. +cant_send_to_fediverse_account: ce n'est pas possible pour le moment :-( +it_not_exists: n'existe pas... +game_already_started: vous avez déjà initié une partie! +wait_other_player: attendez l'autre joueur +is_not_legal_move: c'est un mouvement illégal. Rejouez. +check_done: vous a fait échec! +check_mate: Échec et mat! (en +check_mate_movements: mouvement) +the_winner_is: Le vainqueur est: +well_done: bien joué! +winned_games: Parties gagnés: +wins_of_many: de +lost_piece: * vous avez perdu +not_legal_move_str: mouvement illégal! +player_leave_game: a abandonné la partie +leave_waiting_game: vous avez abandonné la partie en attente. +started_games: parties initiées: +game_is_waiting: en attente... +game_is_on_going: (en jeu) +no_on_going_games: aucune partie en cours +is_not_your_turn: ce n'est pas votre tour. +is_the_turn_of: c'est le tour de +pawn_piece: un pion +knight_piece: un cheval +bishop_piece: le fou +rook_piece: une tour +queen_piece: la Dame +king_piece: le Roi +pawn_piece_letter: P +knight_piece_letter: C +bishop_piece_letter: A +rook_piece_letter: T +queen_piece_letter: D +king_piece_letter: R +email_subject: Annotations partie n. +start_or_join_a_new_game: nouvelle (initier une partie ou en rejoindre une en attente) +move_a_piece: déplace e2e3 (par exemple) +leave_a_game: fin (pour laisser la partie à n'importe quel moment) +list_games: parties (montre une liste des parties en cours) +get_a_game_anotation: envoyer 1 (envoie la partie en format pgn.) +show_help: aide (montre cette aide, et donc, est l'aide de l'aide) +stalemate_str: nulle! partie terminé. +ask_for_draw: nulle +claim_draw_str: ha proposé une nulle a +draw_and_str: et +agreed_draw_str: on accordé une nulle. +claim_a_draw: nulle (por proposer/accepter une nulle) +search_panel: panneau +panel_title_str: Panneau de +panel_games_str: Parties +panel_wins_str: Victoires +panel_ratio_str: Ratio +post_my_panel_str: panneau (publie les statistiques) -- 2.34.1 From ca9ec58dd38b74767d234849ccc0e14942b5f501 Mon Sep 17 00:00:00 2001 From: spla Date: Sat, 19 Dec 2020 21:00:42 +0100 Subject: [PATCH 44/51] New feature! every player can configure bot's replies language! --- README.md | 31 +- app/locales/ca.txt | 4 + app/locales/en.txt | 4 + app/locales/es.txt | 4 + app/locales/fr.txt | 4 + db-setup.py | 4 + mastochess.py | 767 +++++++++++++++++++++++++++++++-------------- 7 files changed, 567 insertions(+), 251 deletions(-) diff --git a/README.md b/README.md index f514eda..69003f0 100644 --- a/README.md +++ b/README.md @@ -44,20 +44,25 @@ Don't use q for queen. Pawn is promoted to Queen by default. - To get your panel stats: -@your_bot_username panel +@your_bot_username panel + +- To change the bot's language: + +@your_bot_username conf en ### Commands table -| ca | en | es | ex. | Observ. | -|:-----:|:-----:|:--------:|:----:|:-----------:| -| nova | new | nueva | | | -| mou | move | mueve | e2e3 | | -| fi | end | fin | | | -| jocs | games | partidas | | | -| envia | send | envia | 1 | game number | -| taules| draw | tablas | | | -| ajuda | help | ayuda | | | -| panell| panel | panel | | | +| ca | en | es | fr | ex. | Observ. | +|:-----:|:-----:|:--------:|:--------:|:----:|:-----------:| +| nova | new | nueva | nouvelle | | | +| mou | move | mueve | déplace | e2e3 | | +| fi | end | fin | fin | | | +| jocs | games | partidas | parties | | | +| envia | send | envia | envoyer | 1 | game number | +| taules| draw | tablas | nulle | | | +| ajuda | help | ayuda | aide | | | +| panell| panel | panel | panneau | | | +| conf | conf | conf | conf | | ca,es,fr,en | ### Dependencies @@ -92,4 +97,6 @@ Within Python Virtual Environment: 28.11.2020 - New feature! Added help 03.12.2020 - New feature! Added pgn save & send support 04.12.2020 - New feature! Now players can claim a draw. -05.12.2020 - New feature! Add panel stats. +05.12.2020 - New feature! Add panel stats. +19.12.2020 - New feature! Now you can configure bot's language! +19.12.2020 - New feature! Added french language! diff --git a/app/locales/ca.txt b/app/locales/ca.txt index e4862ca..e86bfe2 100644 --- a/app/locales/ca.txt +++ b/app/locales/ca.txt @@ -3,6 +3,7 @@ search_move: mou search_new: nova search_games: jocs search_send: envia +search_config: conf search_help: ajuda search_draw: taules new_game_started: partida iniciada! Esperant jugador... @@ -67,3 +68,6 @@ panel_games_str: Partides panel_wins_str: Victòries panel_ratio_str: Ràtio post_my_panel_str: panell (publica les estadístiques) +locale_change_successfully: llengua canviada amb èxit a +locale_not_changed: encara no és suportada :-( +change_lang_str: conf ca (per a configurar el bot en català) diff --git a/app/locales/en.txt b/app/locales/en.txt index 56532ea..70cb02d 100644 --- a/app/locales/en.txt +++ b/app/locales/en.txt @@ -3,6 +3,7 @@ search_move: move search_new: new search_games: games search_send: send +search_config: conf search_help: help search_draw: draw new_game_started: game started! Waiting for the second player... @@ -67,3 +68,6 @@ panel_games_str: Games panel_wins_str: Wins panel_ratio_str: Ratio post_my_panel_str: panel (post player stats) +locale_change_successfully: language sucessfully changed to +locale_not_changed: is not supported yet :-( +change_lang_str: conf en (to configure the bot in english) diff --git a/app/locales/es.txt b/app/locales/es.txt index 7341159..82e8b74 100644 --- a/app/locales/es.txt +++ b/app/locales/es.txt @@ -3,6 +3,7 @@ search_move: mueve search_new: nueva search_games: partidas search_send: envia +search_config: conf search_help: ayuda search_draw: tablas new_game_started: partida iniciada! Esperando jugador... @@ -67,3 +68,6 @@ panel_games_str: Partidas panel_wins_str: Victorias panel_ratio_str: Ratio post_my_panel_str: panel (publica el panel de datos) +locale_change_successfully: idioma cambiado con éxito a +locale_not_changed: no es soportado aún :-( +change_lang_str: conf es (para configurar el bot en castellano) diff --git a/app/locales/fr.txt b/app/locales/fr.txt index cde460e..56f027f 100644 --- a/app/locales/fr.txt +++ b/app/locales/fr.txt @@ -3,6 +3,7 @@ search_move: déplace search_new: nouvelle search_games: parties search_send: envoyer +search_config: conf search_help: aide search_draw: nulle new_game_started: partie initié! En attente d'un joueur... @@ -67,3 +68,6 @@ panel_games_str: Parties panel_wins_str: Victoires panel_ratio_str: Ratio post_my_panel_str: panneau (publie les statistiques) +locale_change_successfully: langue modifiée avec succès en +locale_not_changed: n'est pas encore pris en charge :-( +change_lang_str: conf fr (pour configurer le bot en français) diff --git a/db-setup.py b/db-setup.py index 14a42d4..bf8763c 100644 --- a/db-setup.py +++ b/db-setup.py @@ -168,6 +168,10 @@ if __name__ == '__main__': sql += "finished boolean default False, updated_at timestamptz, CONSTRAINT fk_game FOREIGN KEY(game_id) REFERENCES games(game_id) ON DELETE CASCADE ON UPDATE CASCADE)" create_table(db, db_user, table, sql) + table = "players" + sql = "create table "+table+" (player_id bigint PRIMARY KEY, player_name varchar(40), lang varchar(2))" + create_table(db, db_user, table, sql) + ############################################################ print("Done!") diff --git a/mastochess.py b/mastochess.py index 4ba7093..cc88d15 100644 --- a/mastochess.py +++ b/mastochess.py @@ -20,6 +20,7 @@ import chess.svg from cairosvg import svg2png import chess.pgn from PIL import Image, ImageFont, ImageDraw +import lichess.api def cleanhtml(raw_html): cleanr = re.compile('<.*?>') @@ -48,6 +49,8 @@ def create_panel(username, played_games, wins): large, high = fons.size + panel_title_str = get_locale("panel_title_str", player_lang) + title_length = len(panel_title_str + ' ' + username) # add chess icon @@ -71,6 +74,10 @@ def create_panel(username, played_games, wins): fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 35, layout_engine=ImageFont.LAYOUT_BASIC) + panel_games_str = get_locale("panel_games_str", player_lang) + panel_wins_str = get_locale("panel_wins_str", player_lang) + panel_ratio_str = get_locale("panel_ratio_str", player_lang) + draw.text((y+70,x+120), panel_games_str + ': ' + str(played_games), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity draw.text((y+70,x+170), panel_wins_str + ': ' + str(wins), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity draw.text((y+70,x+220), panel_ratio_str + ': ' + str(ratio) + '%', font=fnt, fill=(255,255,255,220)) @@ -155,89 +162,275 @@ def get_piece_name(captured_piece): if captured_piece == 1: + pawn_piece = get_locale("pawn_piece", player_lang) + piece_name = pawn_piece if captured_piece == 2: + knight_piece = get_locale("knight_piece", player_lang) + piece_name = knight_piece if captured_piece == 3: + bishop_piece = get_locale("bishop_piece", player_lang) + piece_name = bishop_piece if captured_piece == 4: + rook_piece = get_locale("rook_piece", player_lang) + piece_name = rook_piece if captured_piece == 5: + queen_piece = get_locale("queen_piece", player_lang) + piece_name = queen_piece if captured_piece == 6: + king_piece = get_locale("king_piece", player_lang) + piece_name = king_piece return piece_name -def get_notification_data(): +def get_mentions(): + + account_id_lst = [] + + status_id_lst = [] conn = None try: - account_id_lst = [] - - status_id_lst = [] - - text_lst = [] - - visibility_lst = [] - - url_lst = [] - - search_text = [search_end, search_move, search_new, search_games, search_send, search_help, search_draw, search_panel] - conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") cur = conn.cursor() - i=0 + select_query = "select account_id, id from statuses where created_at + interval '60 minutes' > now() - interval '5 minutes'" + select_query += " and id=any (select status_id from mentions where account_id=(%s)) order by created_at asc" - while i < len(search_text): + cur.execute(select_query, (str(bot_id),)) - like_text = "%"+search_text[i]+"%" + rows = cur.fetchall() - select_query = "select account_id, id, text, visibility, url, created_at from statuses where text like (%s) and created_at + interval '60 minutes' > now() - interval '5 minutes'" - select_query += " and id=any (select status_id from mentions where account_id=(%s)) order by created_at asc" + for row in rows: - cur.execute(select_query, (like_text, str(bot_id))) + replied = check_replies(row[1]) - rows = cur.fetchall() - - for row in rows: + if not replied: account_id_lst.append(row[0]) status_id_lst.append(row[1]) - text_lst.append(row[2]) + cur.close() - if row[3] == 0: - visibility_lst.append('public') - elif row[3] == 1: - visibility_lst.append('unlisted') - elif row[3] == 2: - visibility_lst.append('private') - elif row[3] == 3: - visibility_lst.append('direct') + return (account_id_lst, status_id_lst) - url_lst.append(row[4]) + except (Exception, psycopg2.DatabaseError) as error: - i += 1 + print(error) + + finally: + + if conn is not None: + + conn.close() + + +def get_mention_langs(account_id): + + lang_changed = False + + conn = None + + try: + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + select_query = "select lang from players where player_id = (%s)" + + cur.execute(select_query, (str(account_id),)) + + row = cur.fetchone() + + if row != None: + + player_lang = row[0] + + else: + + lang_changed, player_lang = set_lang (account_id, username, bot_lang) cur.close() - return (account_id_lst, status_id_lst, text_lst, visibility_lst, url_lst) + return (lang_changed, player_lang) + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +def get_lang(player): + + conn = None + + try: + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + select_query = "select lang from players where player_name = (%s)" + + cur.execute(select_query, (player,)) + + row = cur.fetchone() + + if row != None: + + player_lang = row[0] + + cur.close() + + return (player_lang) + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + + +def set_lang(account_id, username, new_lang): + + lang_changed = False + + conn = None + + try: + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + select_query = "select lang from players where player_id = (%s)" + + cur.execute(select_query, (str(account_id),)) + + row = cur.fetchone() + + if row != None: + + player_lang = row[0] + + if new_lang not in ['ca', 'es', 'fr', 'en']: + + return lang_changed, player_lang + + if row == None: + + insert_sql = "insert into players(player_id, player_name, lang) values(%s, %s, %s) ON CONFLICT DO NOTHING" + + cur.execute(insert_sql, (account_id, username, new_lang)) + + lang_changed = True + + else: + + update_sql = "update players set lang=(%s) where player_id=(%s)" + + cur.execute(update_sql, (new_lang, account_id)) + + lang_changed = True + + conn.commit() + + cur.close() + + player_lang = new_lang + + return (lang_changed, player_lang) + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +def get_locale( parameter, player_lang): + + if player_lang not in ['ca','es','fr','en']: + print("lang must be 'ca', 'es', 'fr' or 'en'") + sys.exit(0) + + language_filepath = f"app/locales/{player_lang}.txt" + + if not os.path.isfile(language_filepath): + print("File %s not found, exiting."%language_filepath) + sys.exit(0) + + with open( language_filepath ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + print(language_filepath + " Missing parameter %s "%parameter) + sys.exit(0) + +def get_notification_data(status_id): + + conn = None + + try: + + conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + select_query = "select text, visibility, url from statuses where id=(%s)" + + cur.execute(select_query, (status_id,)) + + row = cur.fetchone() + + text = row[0] + + if row[1] == 0: + visibility = 'public' + elif row[1] == 1: + visibility = 'unlisted' + elif row[1] == 2: + visibility = 'private' + elif row[1] == 3: + visibility = 'direct' + + url = row[2] + + cur.close() + + return (text, visibility, url) except (Exception, psycopg2.DatabaseError) as error: @@ -647,6 +840,10 @@ def send_anotation(game_id): # Declare message elements msg['From'] = smtp_user_login msg['To'] = username_email + + player_lang = get_lang(username) + email_subject = get_locale("email_subject", player_lang) + msg['Subject'] = email_subject + game_id # Attach the game anotation @@ -728,13 +925,19 @@ def claim_draw(username): if white_player == username: + username_lang = get_lang(username) + claim_draw_str = get_locale("claim_draw_str", username_lang) + toot_text = '@'+username + ' ' + claim_draw_str + ' @'+black_player + '\n' cur.execute("update games set white_stalemate = 't' where game_id=(%s)", (game_id,)) else: - toot_text = '@'+username + ' ha proposat taules a ' + '@'+white_player + '\n' + username_lang = get_lang(username) + claim_draw_str = get_locale("claim_draw_str", username_lang) + + toot_text = '@'+username + ' ' + claim_draw_str + ' @'+white_player + '\n' cur.execute("update games set black_stalemate = 't' where game_id=(%s)", (game_id,)) @@ -1089,12 +1292,6 @@ def toot_help(): fons = Image.open('app/panel/fons.jpg') print(fons.size) - # add chess icon - #icon_path = 'app/panel/chess.png' - #icon_img = Image.open(icon_path) - - #fons.paste(icon_img, (y+350, x+50), icon_img) - logo_img = Image.open('app/panel/logo.png') fons.paste(logo_img, (15, 320), logo_img) @@ -1106,17 +1303,27 @@ def toot_help(): # get a drawing context draw = ImageDraw.Draw(txt) - draw.text((y+270,x+20), search_help, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+270,x+10), search_help, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 18, layout_engine=ImageFont.LAYOUT_BASIC) - draw.text((y+80,x+80), '@'+bot_username + ' ' + start_or_join_a_new_game , font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity - draw.text((y+80,x+110), '@'+bot_username + ' ' + move_a_piece, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity - draw.text((y+80,x+140), '@'+bot_username + ' ' + leave_a_game, font=fnt, fill=(255,255,255,220)) - draw.text((y+80,x+170), '@'+bot_username + ' ' + list_games, font=fnt, fill=(255,255,255,220)) - draw.text((y+80,x+200), '@'+bot_username + ' ' + get_a_game_anotation, font=fnt, fill=(255,255,255,220)) - draw.text((y+80,x+230), '@'+bot_username + ' ' + claim_a_draw, font=fnt, fill=(255,255,255,220)) - draw.text((y+80,x+260), '@'+bot_username + ' ' + post_my_panel, font=fnt, fill=(255,255,255,220)) + start_or_join_a_new_game = get_locale("start_or_join_a_new_game", player_lang) + move_a_piece = get_locale("move_a_piece", player_lang) + leave_a_game = get_locale("leave_a_game", player_lang) + list_games = get_locale("list_games", player_lang) + get_a_game_anotation = get_locale("get_a_game_anotation", player_lang) + claim_a_draw = get_locale("claim_a_draw", player_lang) + post_my_panel_str = get_locale("post_my_panel_str", player_lang) + change_lang_str = get_locale("change_lang_str", player_lang) + + draw.text((y+80,x+70), '@'+bot_username + ' ' + start_or_join_a_new_game , font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+80,x+100), '@'+bot_username + ' ' + move_a_piece, font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+80,x+130), '@'+bot_username + ' ' + leave_a_game, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+160), '@'+bot_username + ' ' + list_games, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+190), '@'+bot_username + ' ' + get_a_game_anotation, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+220), '@'+bot_username + ' ' + claim_a_draw, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+250), '@'+bot_username + ' ' + post_my_panel_str, font=fnt, fill=(255,255,255,220)) + draw.text((y+80,x+280), '@'+bot_username + ' ' + change_lang_str, font=fnt, fill=(255,255,255,220)) fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 15, layout_engine=ImageFont.LAYOUT_BASIC) @@ -1157,44 +1364,46 @@ def replying(): query_word = question query_word_length = len(query_word) - if unidecode.unidecode(question)[0:query_word_length] == query_word: + if query_word == search_new: - if query_word == search_new: + reply = True - reply = True + elif query_word[:search_move_slicing] == search_move: - elif query_word[:search_move_slicing] == search_move: + moving = query_word[moving_slicing:query_word_length].replace(" ","") + reply = True - moving = query_word[moving_slicing:query_word_length].replace(" ","") - reply = True + elif query_word == search_end: - elif query_word == search_end: + reply = True - reply = True + elif query_word == search_games: - elif query_word == search_games: + reply = True - reply = True + elif query_word[:search_send_slicing] == search_send: - elif query_word[:search_send_slicing] == search_send: + reply = True - reply = True + elif query_word == search_help: - elif query_word == search_help: + reply = True - reply = True + elif query_word == search_draw: - elif query_word == search_draw: + reply = True - reply = True + elif query_word == search_panel: - elif query_word == search_panel: + reply = True - reply = True + elif query_word[:4] == search_config: - else: + reply = True - reply = False + else: + + reply = False return (reply, query_word, moving) @@ -1202,114 +1411,6 @@ def replying(): print(v_error) -def load_strings(bot_lang): - - search_end = get_parameter("search_end", language_filepath) - search_move = get_parameter("search_move", language_filepath) - search_new = get_parameter("search_new", language_filepath) - search_games = get_parameter("search_games", language_filepath) - search_send = get_parameter("search_send", language_filepath) - search_help = get_parameter("search_help", language_filepath) - new_game_started = get_parameter("new_game_started", language_filepath) - playing_with = get_parameter("playing_with", language_filepath) - your_turn = get_parameter("your_turn", language_filepath) - game_name = get_parameter("game_name", language_filepath) - chess_hashtag = get_parameter("chess_hashtag", language_filepath) - send_error = get_parameter("send_error", language_filepath) - - return (search_end, search_move, search_new, search_games, search_send, search_help, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error) - -def load_strings1(bot_lang): - - game_number_anotations = get_parameter("game_number_anotations", language_filepath) - anotations_sent = get_parameter("anotations_sent", language_filepath) - game_no_exists = get_parameter("game_no_exists", language_filepath) - it_not_exists = get_parameter("it_not_exists", language_filepath) - game_already_started = get_parameter("game_already_started", language_filepath) - cant_send_to_fediverse_account = get_parameter("cant_send_to_fediverse_account", language_filepath) - wait_other_player = get_parameter("wait_other_player", language_filepath) - is_not_legal_move = get_parameter("is_not_legal_move", language_filepath) - check_done = get_parameter("check_done", language_filepath) - check_mate = get_parameter("check_mate", language_filepath) - - return (game_number_anotations, anotations_sent, game_no_exists, cant_send_to_fediverse_account, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate) - -def load_strings2(bot_lang): - - check_mate_movements = get_parameter("check_mate_movements", language_filepath) - the_winner_is = get_parameter("the_winner_is", language_filepath) - well_done = get_parameter("well_done", language_filepath) - winned_games = get_parameter("winned_games", language_filepath) - wins_of_many = get_parameter("wins_of_many", language_filepath) - lost_piece = get_parameter("lost_piece", language_filepath) - not_legal_move_str = get_parameter("not_legal_move_str", language_filepath) - player_leave_game = get_parameter("player_leave_game", language_filepath) - - return (check_mate_movements, the_winner_is, well_done, winned_games, wins_of_many, lost_piece, not_legal_move_str, player_leave_game) - -def load_strings3(bot_lang): - - leave_waiting_game = get_parameter("leave_waiting_game", language_filepath) - started_games = get_parameter("started_games", language_filepath) - game_is_waiting = get_parameter("game_is_waiting", language_filepath) - game_is_on_going = get_parameter("game_is_on_going", language_filepath) - no_on_going_games = get_parameter("no_on_going_games", language_filepath) - is_not_your_turn = get_parameter("is_not_your_turn", language_filepath) - is_the_turn_of = get_parameter("is_the_turn_of", language_filepath) - - return (leave_waiting_game, started_games, game_is_waiting, game_is_on_going, no_on_going_games, is_not_your_turn, is_the_turn_of) - -def load_strings4(bot_lang): - - pawn_piece = get_parameter("pawn_piece", language_filepath) - knight_piece = get_parameter("knight_piece", language_filepath) - bishop_piece = get_parameter("bishop_piece", language_filepath) - rook_piece = get_parameter("rook_piece", language_filepath) - queen_piece = get_parameter("queen_piece", language_filepath) - king_piece = get_parameter("king_piece", language_filepath) - - return (pawn_piece, knight_piece, bishop_piece, rook_piece, queen_piece, king_piece) - -def load_strings5(bot_lang): - - pawn_piece_letter = get_parameter("pawn_piece_letter", language_filepath) - knight_piece_letter = get_parameter("knight_piece_letter", language_filepath) - bishop_piece_letter = get_parameter("bishop_piece_letter", language_filepath) - rook_piece_letter = get_parameter("rook_piece_letter", language_filepath) - queen_piece_letter = get_parameter("queen_piece_letter", language_filepath) - king_piece_letter = get_parameter("king_piece_letter", language_filepath) - email_subject = get_parameter("email_subject", language_filepath) - - return (pawn_piece_letter, knight_piece_letter, bishop_piece_letter, rook_piece_letter, queen_piece_letter, king_piece_letter, email_subject) - -def load_strings6(bot_lang): - - start_or_join_a_new_game = get_parameter("start_or_join_a_new_game", language_filepath) - move_a_piece = get_parameter("move_a_piece", language_filepath) - leave_a_game = get_parameter("leave_a_game", language_filepath) - list_games = get_parameter("list_games", language_filepath) - get_a_game_anotation = get_parameter("get_a_game_anotation", language_filepath) - show_help = get_parameter("show_help", language_filepath) - search_draw = get_parameter("search_draw", language_filepath) - ask_for_draw = get_parameter("ask_for_draw", language_filepath) - - return (start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help, search_draw, ask_for_draw) - -def load_strings7(bot_lang): - - claim_draw_str = get_parameter("claim_draw_str", language_filepath) - draw_and_str = get_parameter("draw_and_str", language_filepath) - agreed_draw_str = get_parameter("agreed_draw_str", language_filepath) - claim_a_draw = get_parameter("claim_a_draw", language_filepath) - search_panel = get_parameter("search_panel", language_filepath) - panel_title_str = get_parameter("panel_title_str", language_filepath) - panel_games_str = get_parameter("panel_games_str", language_filepath) - panel_wins_str = get_parameter("panel_wins_str", language_filepath) - panel_ratio_str = get_parameter("panel_ratio_str", language_filepath) - post_my_panel_str = get_parameter("post_my_panel_str", language_filepath) - - return (claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str, post_my_panel_str) - def mastodon(): # Load secrets from secrets file @@ -1389,73 +1490,35 @@ if __name__ == '__main__': if sys.argv[1] == '--play': + bot_lang = '' + if len(sys.argv) == 3: - if sys.argv[2] == '--en': + if sys.argv[2] == '--ca': + + bot_lang = 'ca' + + if sys.argv[2] == '--es': + + bot_lang = 'es' + + if sys.argv[2] == '--fr': + + bot_lang = 'fr' + + elif sys.argv[2] == '--en': bot_lang = 'en' - elif sys.argv[2] == '--es': - - bot_lang = 'es' - else: + elif len(sys.argv) == 2: bot_lang = 'ca' - if bot_lang == 'ca': + if not bot_lang in ['ca', 'es', 'fr', 'en']: - language_filepath = 'app/locales/ca.txt' - - elif bot_lang == 'en': - - language_filepath = 'app/locales/en.txt' - - elif bot_lang == 'es': - - language_filepath = 'app/locales/es.txt' - - else: - - print("\nOnly 'ca', 'es' and 'en' languages are supported.\n") + print("\nOnly 'ca', 'es', 'fr' and 'en' languages are supported.\n") sys.exit(0) - if bot_lang == 'ca': - - search_move_slicing = 3 - moving_slicing = 3 - search_send_slicing = 5 - send_game_slicing = 6 - - elif bot_lang == 'en': - - search_move_slicing = 4 - moving_slicing = 4 - search_send_slicing = 4 - send_game_slicing = 5 - - elif bot_lang == 'es': - - search_move_slicing = 5 - moving_slicing = 5 - search_send_slicing = 5 - send_game_slicing = 6 - - search_end, search_move, search_new, search_games, search_send, search_help, new_game_started, playing_with, your_turn, game_name, chess_hashtag, send_error = load_strings(bot_lang) - - game_number_anotations, anotations_sent, game_no_exists, cant_send_to_fediverse_account, it_not_exists, game_already_started, wait_other_player, is_not_legal_move, check_done, check_mate = load_strings1(bot_lang) - - check_mate_movements, the_winner_is, well_done, winned_games, wins_of_many, lost_piece, not_legal_move_str, player_leave_game = load_strings2(bot_lang) - - leave_waiting_game, started_games, game_is_waiting, game_is_on_going, no_on_going_games, is_not_your_turn, is_the_turn_of = load_strings3(bot_lang) - - pawn_piece, knight_piece, bishop_piece, rook_piece, queen_piece, king_piece = load_strings4(bot_lang) - - pawn_piece_letter, knight_piece_letter, bishop_piece_letter, rook_piece_letter, queen_piece_letter, king_piece_letter, email_subject = load_strings5(bot_lang) - - start_or_join_a_new_game, move_a_piece, leave_a_game, list_games, get_a_game_anotation, show_help, search_draw, ask_for_draw = load_strings6(bot_lang) - - claim_draw_str, draw_and_str, agreed_draw_str, claim_a_draw, search_panel, panel_title_str, panel_games_str, panel_wins_str, panel_ratio_str, post_my_panel = load_strings7(bot_lang) - mastodon, mastodon_hostname, bot_username = mastodon() mastodon_db, mastodon_db_user, chess_db, chess_db_user = db_config() @@ -1466,15 +1529,59 @@ if __name__ == '__main__': bot_id = get_bot_id() - account_id_lst, status_id_lst, text_lst, visibility_lst, url_lst = get_notification_data() + account_id_lst, status_id_lst = get_mentions() + + if len(account_id_lst) == 0: + + print('No mentions') + sys.exit(0) i = 0 + while i < len(account_id_lst): account_id = account_id_lst[i] username, domain = get_user_domain(account_id) + lang_changed, player_lang = get_mention_langs(account_id) + + if player_lang == 'ca': + + search_move_slicing = 3 + moving_slicing = 3 + search_send_slicing = 5 + send_game_slicing = 6 + + elif player_lang == 'en': + + search_move_slicing = 4 + moving_slicing = 4 + search_send_slicing = 4 + send_game_slicing = 5 + + elif player_lang == 'es': + + search_move_slicing = 5 + moving_slicing = 5 + search_send_slicing = 5 + send_game_slicing = 6 + + elif player_lang == 'fr': + + search_move_slicing = 7 + moving_slicing = 7 + search_send_slicing = 7 + send_game_slicing = 7 + + else: + + sys.exit(0) + + status_id = status_id_lst[i] + + text, visibility, url = get_notification_data(status_id) + if domain != None: username = username + '@' + domain @@ -1491,13 +1598,19 @@ if __name__ == '__main__': # listen them or not - text = text_lst[i] + search_new = get_locale("search_new", player_lang) + search_move = get_locale("search_move", player_lang) + search_end = get_locale("search_end", player_lang) + search_games = get_locale("search_games", player_lang) + search_send = get_locale("search_send", player_lang) + search_help = get_locale("search_help", player_lang) + search_draw = get_locale("search_draw", player_lang) + search_panel = get_locale("search_panel", player_lang) + search_config = get_locale("search_config", player_lang) reply, query_word, moving = replying() - visibility = visibility_lst[i] - - status_url = url_lst[i] + status_url = url if query_word != search_games: @@ -1523,6 +1636,8 @@ if __name__ == '__main__': svg2png(bytestring=svgfile,write_to=board_file) + new_game_started = get_locale("new_game_started", player_lang) + toot_text = '@'+username + ' ' + new_game_started + '\n' toot_text += '\n' @@ -1553,14 +1668,25 @@ if __name__ == '__main__': svg2png(bytestring=svgfile,write_to=board_file) + player_lang1 = get_lang(username) + + playing_with = get_locale("playing_with", player_lang1) + toot_text = '@'+username + ' ' + playing_with + ' ' + white_user + "\n" toot_text += '\n' + player_lang2 = get_lang(white_user) + + your_turn = get_locale("your_turn", player_lang2) + toot_text += '@'+white_user + ': ' + your_turn + "\n" toot_text += '\n' + game_name = get_locale("game_name", player_lang2) + chess_hashtag = get_locale("chess_hashtag", player_lang) + toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' image_id = mastodon.media_post(board_file, "image/png").id @@ -1583,20 +1709,34 @@ if __name__ == '__main__': if emailed == False and game_found == True: + username_lang = get_lang(username) + send_error = get_locale("send_error", username_lang) + toot_text = '@'+username + ' ' + send_error elif emailed == True and game_found == True: + username_lang = get_lang(username) + game_number_anotations = get_locale("game_number_anotations", username_lang) + anotations_sent = get_locale("anotations_sent", username_lang) + toot_text = '@'+username + ' ' + game_number_anotations + str(game_id) + ' ' + anotations_sent elif emailed == False and game_found == False: if domain != None: + username_lang = get_lang(username) + cant_send_to_fediverse_account = get_locale("cant_send_to_fediverse_account", username_lang) + toot_text = '@'+username + ' ' + cant_send_to_fediverse_account else: + username_lang = get_lang(username) + game_no_exists = get_locale("game_no_exists", username_lang) + it_not_exists = get_locale("it_not_exists", username_lang) + toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1619,6 +1759,30 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word[:4] == search_config: + + new_lang = query_word[5:7] + + lang_changed, player_lang = set_lang(account_id, username, new_lang) + + update_replies(status_id, username, now) + + if lang_changed: + + locale_change_successfully = get_locale("locale_change_successfully", new_lang) + + toot_text = '@'+username + ' ' + locale_change_successfully + ' ' + new_lang + + mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) + + else: + + locale_not_changed = get_locale("locale_not_changed", player_lang) + + toot_text = '@'+username + ' ' + new_lang + ' ' + locale_not_changed + + mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) + elif query_word == search_help: toot_help() @@ -1641,6 +1805,10 @@ if __name__ == '__main__': if query_word == search_new: + player_lang1 = get_lang(username) + + game_already_started = get_locale("game_already_started", player_lang1) + toot_text = '@'+username + ' ' + game_already_started + '\n' if black_user != '': @@ -1649,10 +1817,15 @@ if __name__ == '__main__': else: + wait_other_player = get_locale("wait_other_player", player_lang1) + toot_text += wait_other_player + '\n' toot_text += '\n' + game_name = get_locale("game_name", player_lang1) + chess_hashtag = get_locale("chess_hashtag", player_lang1) + toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' board = chess.Board(on_going_game) @@ -1713,6 +1886,8 @@ if __name__ == '__main__': if not_legal_move: + is_not_legal_move = get_locale("is_not_legal_move", player_lang) + toot_text = '@'+username + ': ' + moving + ' ' + is_not_legal_move + '\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1775,22 +1950,45 @@ if __name__ == '__main__': if check == True and checkmate == False: + player_lang = get_lang(playing_user) + check_done = get_locale("check_done", player_lang) + toot_text = "@"+playing_user + " " + username + ' ' + check_done + '\n' elif check == True and checkmate == True: + player_lang1 = get_lang(username) + + check_mate = get_locale("check_mate", player_lang1) + check_mate_movements = get_locale("check_mate_movements", player_lang1) + the_winner_is = get_locale("the_winner_is", player_lang1) + toot_text = '\n' + check_mate + ' ' + str(game_moves) + ' ' + check_mate_movements + '\n\n' + the_winner_is + ' ' + "@"+username + '\n' - toot_text += "\n@"+playing_user + ': ' + well_done + "\n" + winned_games = get_locale("winned_games", player_lang1) toot_text += '\n' + winned_games + "\n" played_games, wins = get_stats(username) + wins_of_many = get_locale("wins_of_many", player_lang1) + toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + player_lang2 = get_lang(playing_user) + + well_done = get_locale("well_done", player_lang2) + + toot_text += "\n@"+playing_user + ': ' + well_done + "\n" + played_games, wins = get_stats(playing_user) + winned_games = get_locale("winned_games", player_lang2) + + wins_of_many = get_locale("wins_of_many", player_lang2) + + toot_text += '\n\n' + winned_games + "\n" + toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" elif check == False and stalemate == True: @@ -1811,12 +2009,24 @@ if __name__ == '__main__': else: + player_lang = get_lang(playing_user) + + your_turn = get_locale("your_turn", player_lang) + toot_text = '@'+playing_user + ' ' + your_turn + '\n' if capture == True and checkmate == False: + player_lang = get_lang(playing_user) + lost_piece = get_locale("lost_piece", player_lang) + piece_name = get_locale("piece_name", player_lang) + toot_text += '\n' + lost_piece + ' ' + piece_name + '!\n' + game_name = get_locale("game_name", player_lang) + + chess_hashtag = get_locale("chess_hashtag", player_lang) + toot_text += '\n' + game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' if username == white_user: @@ -1865,6 +2075,9 @@ if __name__ == '__main__': print(v_error) + username_lang = get_lang(username) + not_legal_move_str = get_locale("not_legal_move_str", username_lang) + toot_text = '@'+username + ' ' + not_legal_move_str + ' ' + moving + '!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1877,6 +2090,9 @@ if __name__ == '__main__': print(a_error) + username_lang = get_lang(username) + not_legal_move_str = get_locale("not_legal_move_str", username_lang) + toot_text = '@'+username + ' ' + not_legal_move_str + ' ' + moving + '!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1895,6 +2111,8 @@ if __name__ == '__main__': if username == white_user: + player_leave_game = get_locale("player_leave_game", player_lang) + toot_text = '@'+username + ' ' + player_leave_game + ' ' + '@'+black_user mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1923,6 +2141,8 @@ if __name__ == '__main__': else: + leave_waiting_game = get_locale("leave_waiting_game", player_lang) + toot_text = '@'+username + ' ' + leave_waiting_game mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) @@ -1941,6 +2161,8 @@ if __name__ == '__main__': if len(player1_name_lst) > 0: + started_games = get_locale("started_games", player_lang) + toot_text = "@"+username + ' ' + started_games + "\n" i = 0 @@ -1948,10 +2170,14 @@ if __name__ == '__main__': if game_status_lst[i] == 'waiting': + game_is_waiting = get_locale("game_is_waiting", player_lang) + toot_text += '\n' + player1_name_lst[i] + ' / ' + player2_name_lst[i] + ' ' + game_is_waiting + "\n" else: + game_is_on_going = get_locale("game_is_on_going", player_lang) + if next_move_lst[i] == player1_name_lst[i]: toot_text += '\n*' + player1_name_lst[i] + ' / ' + player2_name_lst[i] + ' ' + game_is_on_going + '\n' @@ -1970,6 +2196,8 @@ if __name__ == '__main__': else: + no_on_going_games = get_locale("no_on_going_games", player_lang) + toot_text = '@'+username + ' ' + no_on_going_games + '\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -1984,16 +2212,26 @@ if __name__ == '__main__': emailed, game_id, game_found = send_anotation(send_game) + username_lang = get_lang(username) + if emailed == False and game_found == True: + send_error = get_locale("send_error", username_lang) + toot_text = '@'+username + ' ' + send_error elif emailed == True and game_found == True: + game_number_anotations = get_locale("game_number_anotations", username_lang) + anotations_sent = get_locale("anotations_sent", username_lang) + toot_text = '@'+username + ' ' + game_number_anotations + str(game_id) + ' ' + anotations_sent elif emailed == False and game_found == False: + game_no_exists = get_locale("game_no_exists", username_lang) + it_not_exists = get_locale("it_not_exists", username_lang) + toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -2010,6 +2248,12 @@ if __name__ == '__main__': close_game(username, checkmate) + player_lang1 = get_lang(white_player) + draw_and_str = get_locale("draw_and_str", player_lang1) + agreed_draw_str = get_locale("agreed_draw_str", player_lang1) + winned_games = get_locale("winned_games", player_lang1) + wins_of_many = get_locale("wins_of_many", player_lang1) + toot_text = '@'+white_player + ' ' + draw_and_str + ' ' + '@'+black_player + ' ' + agreed_draw_str + '\n\n' toot_text += '\n' + winned_games + "\n" @@ -2018,6 +2262,18 @@ if __name__ == '__main__': toot_text += white_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + player_lang2 = get_lang(black_player) + draw_and_str = get_locale("draw_and_str", player_lang2) + agreed_draw_str = get_locale("agreed_draw_str", player_lang2) + winned_games = get_locale("winned_games", player_lang2) + wins_of_many = get_locale("wins_of_many", player_lang2) + + if player_lang1 != player_lang2: + + toot_text += '\n@'+white_player + ' ' + draw_and_str + ' ' + '@'+black_player + ' ' + agreed_draw_str + '\n\n' + + toot_text += '\n' + winned_games + "\n" + played_games, wins = get_stats(black_player) toot_text += black_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" @@ -2046,6 +2302,30 @@ if __name__ == '__main__': update_replies(status_id, username, now) + elif query_word[:4] == search_config: + + new_lang = query_word[5:7] + + lang_changed, player_lang = set_lang(account_id, username, new_lang) + + update_replies(status_id, username, now) + + if lang_changed: + + locale_change_successfully = get_locale("locale_change_successfully", new_lang) + + toot_text = '@'+username + ' ' + locale_change_successfully + ' ' + new_lang + + mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) + + else: + + locale_not_changed = get_locale("locale_not_changed", player_lang) + + toot_text = '@'+username + ' ' + new_lang + ' ' + locale_not_changed + + mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) + elif query_word == search_help: toot_help() @@ -2064,14 +2344,23 @@ if __name__ == '__main__': if playing_user == None: + username_lang = get_lang(username) + is_not_your_turn = get_locale("is_not_your_turn", username_lang) + toot_text = '@'+username + ' ' + is_not_your_turn + '\n' else: + is_the_turn_of = get_locale("is_the_turn_of", player_lang) + toot_text = '@'+username + ' ' + is_the_turn_of + ' ' + playing_user + "\n" toot_text += '\n' + game_name = get_locale("game_name", player_lang) + + chess_hashtag = get_locale("chess_hashtag", player_lang) + toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' board = chess.Board(on_going_game) -- 2.34.1 From 13ddb3b25b0bb097a39979eb6e40c2bfde211571 Mon Sep 17 00:00:00 2001 From: spla Date: Mon, 21 Dec 2020 10:23:04 +0100 Subject: [PATCH 45/51] New feature! Added Elo rating system! --- README.md | 4 +- app/locales/ca.txt | 1 + app/locales/en.txt | 1 + app/locales/es.txt | 1 + app/locales/fr.txt | 1 + db-setup.py | 2 +- mastochess.py | 133 +++++++++++++++++++++++++++++++++++++++------ 7 files changed, 123 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 69003f0..936a9d8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Mastodon Chess Play with other fediverse users a Chess game! Mastodon Chess control games, players and boards and even it post, graphically, every move to both players! Mastodon Chess (mastochess) uses [python-chess](https://python-chess.readthedocs.io/en/latest/) library. +Mastodon Chess uses Elo rating system to calculate the relative skill levels of fediverse players! ### How to play: @@ -99,4 +100,5 @@ Within Python Virtual Environment: 04.12.2020 - New feature! Now players can claim a draw. 05.12.2020 - New feature! Add panel stats. 19.12.2020 - New feature! Now you can configure bot's language! -19.12.2020 - New feature! Added french language! +19.12.2020 - New feature! Added french language! +21.12.2020 - New feature! Added Elo rating system! diff --git a/app/locales/ca.txt b/app/locales/ca.txt index e86bfe2..36ecd28 100644 --- a/app/locales/ca.txt +++ b/app/locales/ca.txt @@ -71,3 +71,4 @@ post_my_panel_str: panell (publica les estadístiques) locale_change_successfully: llengua canviada amb èxit a locale_not_changed: encara no és suportada :-( change_lang_str: conf ca (per a configurar el bot en català) +panel_elo_rating_str: Elo diff --git a/app/locales/en.txt b/app/locales/en.txt index 70cb02d..8718dd1 100644 --- a/app/locales/en.txt +++ b/app/locales/en.txt @@ -71,3 +71,4 @@ post_my_panel_str: panel (post player stats) locale_change_successfully: language sucessfully changed to locale_not_changed: is not supported yet :-( change_lang_str: conf en (to configure the bot in english) +panel_elo_rating_str: Elo diff --git a/app/locales/es.txt b/app/locales/es.txt index 82e8b74..fea1af5 100644 --- a/app/locales/es.txt +++ b/app/locales/es.txt @@ -71,3 +71,4 @@ post_my_panel_str: panel (publica el panel de datos) locale_change_successfully: idioma cambiado con éxito a locale_not_changed: no es soportado aún :-( change_lang_str: conf es (para configurar el bot en castellano) +panel_elo_rating_str: Elo diff --git a/app/locales/fr.txt b/app/locales/fr.txt index 56f027f..de86e82 100644 --- a/app/locales/fr.txt +++ b/app/locales/fr.txt @@ -71,3 +71,4 @@ post_my_panel_str: panneau (publie les statistiques) locale_change_successfully: langue modifiée avec succès en locale_not_changed: n'est pas encore pris en charge :-( change_lang_str: conf fr (pour configurer le bot en français) +panel_elo_rating_str: Elo diff --git a/db-setup.py b/db-setup.py index bf8763c..10c03f0 100644 --- a/db-setup.py +++ b/db-setup.py @@ -169,7 +169,7 @@ if __name__ == '__main__': create_table(db, db_user, table, sql) table = "players" - sql = "create table "+table+" (player_id bigint PRIMARY KEY, player_name varchar(40), lang varchar(2))" + sql = "create table "+table+" (player_id bigint PRIMARY KEY, player_name varchar(40), lang varchar(2), elo_rating float)" create_table(db, db_user, table, sql) ############################################################ diff --git a/mastochess.py b/mastochess.py index cc88d15..b848417 100644 --- a/mastochess.py +++ b/mastochess.py @@ -20,7 +20,7 @@ import chess.svg from cairosvg import svg2png import chess.pgn from PIL import Image, ImageFont, ImageDraw -import lichess.api +import math def cleanhtml(raw_html): cleanr = re.compile('<.*?>') @@ -31,7 +31,48 @@ def unescape(s): s = s.replace("'", "'") return s -def create_panel(username, played_games, wins): +# Function to calculate the Probability +def Probability(rating1, rating2): + + return 1.0 * 1.0 / (1 + 1.0 * math.pow(10, 1.0 * (rating1 - rating2) / 400)) + +# Function to calculate Elo rating +# K is a constant. +# d determines whether +# Player A wins or Player B. +def EloRating(Ra, Rb, K, d): + + # To calculate the Winning + # Probability of Player B + Pb = Probability(Ra, Rb) + + # To calculate the Winning + # Probability of Player A + Pa = Probability(Rb, Ra) + + # Case -1 When Player A wins + # Updating the Elo Ratings + if (d == 1) : + Ra = Ra + K * (1 - Pa) + Rb = Rb + K * (0 - Pb) + + # Case -2 When Player B wins + # Updating the Elo Ratings + else : + Ra = Ra + K * (0 - Pa) + Rb = Rb + K * (1 - Pb) + + Ra = round(Ra, 6) + Rb = round(Rb, 6) + print("Updated Ratings:-") + print("Ra =", Ra," Rb =", Rb) + + return(Ra, Rb) + + # This code is contributed by + # Smitha Dinesh Semwal + +def create_panel(username, rating, played_games, wins): if played_games > 0 and wins > 0: @@ -77,10 +118,12 @@ def create_panel(username, played_games, wins): panel_games_str = get_locale("panel_games_str", player_lang) panel_wins_str = get_locale("panel_wins_str", player_lang) panel_ratio_str = get_locale("panel_ratio_str", player_lang) + panel_elo_rating_str = get_locale("panel_elo_rating_str", player_lang) - draw.text((y+70,x+120), panel_games_str + ': ' + str(played_games), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity - draw.text((y+70,x+170), panel_wins_str + ': ' + str(wins), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity - draw.text((y+70,x+220), panel_ratio_str + ': ' + str(ratio) + '%', font=fnt, fill=(255,255,255,220)) + draw.text((y+70,x+80), panel_games_str + ': ' + str(played_games), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+70,x+130), panel_wins_str + ': ' + str(wins), font=fnt, fill=(255,255,255,220)) #fill=(255,255,255,255)) ## full opacity + draw.text((y+70,x+180), panel_ratio_str + ': ' + str(ratio) + '%', font=fnt, fill=(255,255,255,220)) + draw.text((y+70,x+230), panel_elo_rating_str + ': ' + str(round(rating)), font=fnt, fill=(255,255,255,220)) fnt = ImageFont.truetype('app/fonts/DroidSans.ttf', 15, layout_engine=ImageFont.LAYOUT_BASIC) @@ -978,6 +1021,8 @@ def claim_draw(username): def close_game(username, checkmate): + d = 0 + try: conn = None @@ -996,6 +1041,30 @@ def close_game(username, checkmate): black_player = row[1] + cur.execute("select elo_rating from players where player_name = (%s)", (white_player,)) + + row = cur.fetchone() + + if row[0] != None: + + white_rating = row[0] + + else: + + white_rating = 1500 + + cur.execute("select elo_rating from players where player_name = (%s)", (black_player,)) + + row = cur.fetchone() + + if row[0] != None: + + black_rating = row[0] + + else: + + black_rating = 1500 + cur.close() except (Exception, psycopg2.DatabaseError) as error: @@ -1028,16 +1097,28 @@ def close_game(username, checkmate): winner = username + if winner == white_player: + + d = 1 + else: if query_word == search_end and username == white_user and stalemate == False: winner = black_user + d = 2 + elif query_word == search_end and username == black_user and stalemate == False: winner = white_user + d = 1 + + K = 30 + + new_white_rating, new_black_rating = EloRating(white_rating, black_rating, K, d) + try: conn = None @@ -1050,6 +1131,10 @@ def close_game(username, checkmate): cur.execute("update stats set winner=(%s), finished=(%s), updated_at=(%s) where game_id=(%s)", (winner, finished, now, game_id)) + cur.execute("update players set elo_rating=(%s) where player_name=(%s)", (new_white_rating, white_user)) + + cur.execute("update players set elo_rating=(%s) where player_name=(%s)", (new_black_rating, black_user)) + conn.commit() cur.close() @@ -1094,9 +1179,21 @@ def get_stats(player): wins = row[0] + cur.execute("select elo_rating from players where player_name = (%s)", (player,)) + + row = cur.fetchone() + + if row[0] != None: + + rating = row[0] + + else: + + rating = 1500 + cur.close() - return (played_games, wins) + return (rating, played_games, wins) except (Exception, psycopg2.DatabaseError) as error: @@ -1745,9 +1842,9 @@ if __name__ == '__main__': elif query_word == search_panel: - played_games, wins = get_stats(username) + rating, played_games, wins = get_stats(username) - create_panel(username, played_games, wins) + create_panel(username, rating, played_games, wins) toot_text = '@'+username @@ -1969,11 +2066,11 @@ if __name__ == '__main__': toot_text += '\n' + winned_games + "\n" - played_games, wins = get_stats(username) + rating, played_games, wins = get_stats(username) wins_of_many = get_locale("wins_of_many", player_lang1) - toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + ' (Elo: ' + str(round(rating)) + ')' + '\n' player_lang2 = get_lang(playing_user) @@ -1981,7 +2078,7 @@ if __name__ == '__main__': toot_text += "\n@"+playing_user + ': ' + well_done + "\n" - played_games, wins = get_stats(playing_user) + rating, played_games, wins = get_stats(playing_user) winned_games = get_locale("winned_games", player_lang2) @@ -1989,7 +2086,7 @@ if __name__ == '__main__': toot_text += '\n\n' + winned_games + "\n" - toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + ' (Elo: ' + str(round(rating)) + ')' + '\n' elif check == False and stalemate == True: @@ -1999,11 +2096,11 @@ if __name__ == '__main__': toot_text += '\n' + winned_games + "\n" - played_games, wins = get_stats(username) + rating, played_games, wins = get_stats(username) toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" - played_games, wins = get_stats(playing_user) + rating, played_games, wins = get_stats(playing_user) toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" @@ -2258,7 +2355,7 @@ if __name__ == '__main__': toot_text += '\n' + winned_games + "\n" - played_games, wins = get_stats(white_player) + rating, played_games, wins = get_stats(white_player) toot_text += white_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" @@ -2274,7 +2371,7 @@ if __name__ == '__main__': toot_text += '\n' + winned_games + "\n" - played_games, wins = get_stats(black_player) + rating, played_games, wins = get_stats(black_player) toot_text += black_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" @@ -2288,9 +2385,9 @@ if __name__ == '__main__': elif query_word == search_panel: - played_games, wins = get_stats(username) + rating, played_games, wins = get_stats(username) - create_panel(username, played_games, wins) + create_panel(username, rating, played_games, wins) toot_text = '@'+username -- 2.34.1 From 845746cf8fc197773946574d876f0a256989cccd Mon Sep 17 00:00:00 2001 From: spla Date: Mon, 28 Dec 2020 14:06:27 +0100 Subject: [PATCH 46/51] Fix #13. Removed unneeded line --- mastochess.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mastochess.py b/mastochess.py index b848417..ea34356 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1,4 +1,3 @@ -import pdb import sys import os import os.path @@ -2116,7 +2115,6 @@ if __name__ == '__main__': player_lang = get_lang(playing_user) lost_piece = get_locale("lost_piece", player_lang) - piece_name = get_locale("piece_name", player_lang) toot_text += '\n' + lost_piece + ' ' + piece_name + '!\n' -- 2.34.1 From 67af0c8f7a8da93904a4f82be0e044c37377ebb0 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 1 Jan 2021 17:44:58 +0100 Subject: [PATCH 47/51] Fix #14 --- mastochess.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mastochess.py b/mastochess.py index ea34356..0107025 100644 --- a/mastochess.py +++ b/mastochess.py @@ -2222,6 +2222,8 @@ if __name__ == '__main__': else: + player_leave_game = get_locale("player_leave_game", player_lang) + toot_text = '@'+username + ' ' + player_leave_game + ' ' + white_user mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) -- 2.34.1 From 7d7ffe33702bbd3b1d8852412a745045a5559e3c Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 1 Jan 2021 18:09:49 +0100 Subject: [PATCH 48/51] Mention two players when any leaves the game --- mastochess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mastochess.py b/mastochess.py index 0107025..8e90031 100644 --- a/mastochess.py +++ b/mastochess.py @@ -2224,7 +2224,7 @@ if __name__ == '__main__': player_leave_game = get_locale("player_leave_game", player_lang) - toot_text = '@'+username + ' ' + player_leave_game + ' ' + white_user + toot_text = '@'+username + ' ' + player_leave_game + ' ' + '@'+white_user mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) -- 2.34.1 From eb2df2d0a8ceb31cc9822a0faca34529f9ce1775 Mon Sep 17 00:00:00 2001 From: spla Date: Fri, 1 Jan 2021 18:59:21 +0100 Subject: [PATCH 49/51] Fix #15 --- mastochess.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/mastochess.py b/mastochess.py index 8e90031..91710d4 100644 --- a/mastochess.py +++ b/mastochess.py @@ -1044,9 +1044,15 @@ def close_game(username, checkmate): row = cur.fetchone() - if row[0] != None: + if row != None: - white_rating = row[0] + if row[0] != None: + + white_rating = row[0] + + else: + + white_rating = 1500 else: @@ -1056,9 +1062,15 @@ def close_game(username, checkmate): row = cur.fetchone() - if row[0] != None: + if row != None: - black_rating = row[0] + if row[0] != None: + + black_rating = row[0] + + else: + + black_rating = 1500 else: -- 2.34.1 From c3027ccd042127bf810a926f3b7fad15f811a1db Mon Sep 17 00:00:00 2001 From: spla Date: Mon, 3 May 2021 15:07:42 +0200 Subject: [PATCH 50/51] Detect if DST is in effect --- mastochess.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mastochess.py b/mastochess.py index 91710d4..bc7dd35 100644 --- a/mastochess.py +++ b/mastochess.py @@ -4,6 +4,7 @@ import os.path import re import unidecode from datetime import datetime, timedelta +import time from mastodon import Mastodon from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -241,11 +242,19 @@ def get_piece_name(captured_piece): return piece_name def get_mentions(): - + account_id_lst = [] status_id_lst = [] + if time.localtime().tm_isdst == 0: + + interval_time = '60 minutes' + + elif time.localtime().tm_isdst == 1: + + interval_time = '120 minutes' + conn = None try: @@ -254,7 +263,7 @@ def get_mentions(): cur = conn.cursor() - select_query = "select account_id, id from statuses where created_at + interval '60 minutes' > now() - interval '5 minutes'" + select_query = "select account_id, id from statuses where created_at + interval '" + interval_time + "' > now() - interval '5 minutes'" select_query += " and id=any (select status_id from mentions where account_id=(%s)) order by created_at asc" cur.execute(select_query, (str(bot_id),)) -- 2.34.1 From 6a5638cfb7706a7915219c6df950597f0de2d4bc Mon Sep 17 00:00:00 2001 From: spla Date: Thu, 17 Mar 2022 12:53:30 +0100 Subject: [PATCH 51/51] Changed notifications management to Mastodon API --- mastochess.py | 452 ++++++++++++----------------------------------- requirements.txt | 1 + 2 files changed, 115 insertions(+), 338 deletions(-) diff --git a/mastochess.py b/mastochess.py index 91710d4..8553c8b 100644 --- a/mastochess.py +++ b/mastochess.py @@ -4,6 +4,7 @@ import os.path import re import unidecode from datetime import datetime, timedelta +import time from mastodon import Mastodon from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -20,6 +21,7 @@ from cairosvg import svg2png import chess.pgn from PIL import Image, ImageFont, ImageDraw import math +import pdb def cleanhtml(raw_html): cleanr = re.compile('<.*?>') @@ -131,75 +133,6 @@ def create_panel(username, rating, played_games, wins): out = Image.alpha_composite(base, txt) out.save('app/panel/' + username + '_panel.png') -def get_bot_id(): - - ################################################################################################################################### - # get bot_id from bot's username - - try: - - conn = None - - conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") - - cur = conn.cursor() - - cur.execute("select id from accounts where username = (%s) and domain is null", (bot_username,)) - - row = cur.fetchone() - - if row != None: - - bot_id = row[0] - - cur.close() - - return bot_id - - except (Exception, psycopg2.DatabaseError) as error: - - print(error) - - finally: - - if conn is not None: - - conn.close() - -def get_user_domain(account_id): - - try: - - conn = None - - conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") - - cur = conn.cursor() - - cur.execute("select username, domain from accounts where id=(%s)", (account_id,)) - - row = cur.fetchone() - - if row != None: - - username = row[0] - - domain = row[1] - - cur.close() - - return (username, domain) - - except (Exception, psycopg2.DatabaseError) as error: - - print(error) - - finally: - - if conn is not None: - - conn.close() - def get_piece_name(captured_piece): if captured_piece == 1: @@ -240,53 +173,7 @@ def get_piece_name(captured_piece): return piece_name -def get_mentions(): - - account_id_lst = [] - - status_id_lst = [] - - conn = None - - try: - - conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") - - cur = conn.cursor() - - select_query = "select account_id, id from statuses where created_at + interval '60 minutes' > now() - interval '5 minutes'" - select_query += " and id=any (select status_id from mentions where account_id=(%s)) order by created_at asc" - - cur.execute(select_query, (str(bot_id),)) - - rows = cur.fetchall() - - for row in rows: - - replied = check_replies(row[1]) - - if not replied: - - account_id_lst.append(row[0]) - - status_id_lst.append(row[1]) - - cur.close() - - return (account_id_lst, status_id_lst) - - except (Exception, psycopg2.DatabaseError) as error: - - print(error) - - finally: - - if conn is not None: - - conn.close() - - -def get_mention_langs(account_id): +def get_player_langs(account_id): lang_changed = False @@ -360,7 +247,6 @@ def get_lang(player): conn.close() - def set_lang(account_id, username, new_lang): lang_changed = False @@ -441,119 +327,6 @@ def get_locale( parameter, player_lang): print(language_filepath + " Missing parameter %s "%parameter) sys.exit(0) -def get_notification_data(status_id): - - conn = None - - try: - - conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432") - - cur = conn.cursor() - - select_query = "select text, visibility, url from statuses where id=(%s)" - - cur.execute(select_query, (status_id,)) - - row = cur.fetchone() - - text = row[0] - - if row[1] == 0: - visibility = 'public' - elif row[1] == 1: - visibility = 'unlisted' - elif row[1] == 2: - visibility = 'private' - elif row[1] == 3: - visibility = 'direct' - - url = row[2] - - cur.close() - - return (text, visibility, url) - - except (Exception, psycopg2.DatabaseError) as error: - - print(error) - - finally: - - if conn is not None: - - conn.close() - -def update_replies(status_id, username, now): - - post_id = status_id - - try: - - conn = None - - conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") - - cur = conn.cursor() - - insert_sql = "insert into botreplies(status_id, query_user, status_created_at) values(%s, %s, %s) ON CONFLICT DO NOTHING" - - cur.execute(insert_sql, (post_id, username, now)) - - conn.commit() - - cur.close() - - except (Exception, psycopg2.DatabaseError) as error: - - sys.exit(error) - - finally: - - if conn is not None: - - conn.close() - -def check_replies(status_id): - - post_id = status_id - - replied = False - - try: - - conn = None - - conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") - - cur = conn.cursor() - - cur.execute("select status_id from botreplies where status_id=(%s)", (post_id,)) - - row = cur.fetchone() - - if row != None: - - replied = True - - else: - - replied = False - - cur.close() - - return replied - - except (Exception, psycopg2.DatabaseError) as error: - - sys.exit(error) - - finally: - - if conn is not None: - - conn.close() - def current_games(): player1_name_lst = [] @@ -1635,24 +1408,46 @@ if __name__ == '__main__': now = datetime.now() - bot_id = get_bot_id() + #################################################################### + # get notifications - account_id_lst, status_id_lst = get_mentions() + notifications = mastodon.notifications() - if len(account_id_lst) == 0: + if len(notifications) == 0: print('No mentions') + sys.exit(0) i = 0 - while i < len(account_id_lst): + while i < len(notifications): - account_id = account_id_lst[i] + notification_id = notifications[i].id - username, domain = get_user_domain(account_id) + if notifications[i].type != 'mention': - lang_changed, player_lang = get_mention_langs(account_id) + i += 1 + + print(f'dismissing notification {notification_id}') + + mastodon.notifications_dismiss(notification_id) + + continue + + account_id = notifications[i].account.id + + username = notifications[i].account.acct + + status_id = notifications[i].status.id + + text = notifications[i].status.content + + visibility = notifications[i].status.visibility + + url = notifications[i].status.uri + + lang_changed, player_lang = get_player_langs(account_id) if player_lang == 'ca': @@ -1686,24 +1481,6 @@ if __name__ == '__main__': sys.exit(0) - status_id = status_id_lst[i] - - text, visibility, url = get_notification_data(status_id) - - if domain != None: - - username = username + '@' + domain - - status_id = status_id_lst[i] - - replied = check_replies(status_id) - - if replied == True: - - i += 1 - - continue - # listen them or not search_new = get_locale("search_new", player_lang) @@ -1746,7 +1523,7 @@ if __name__ == '__main__': new_game_started = get_locale("new_game_started", player_lang) - toot_text = '@'+username + ' ' + new_game_started + '\n' + toot_text = f'@{username} {new_game_started} \n' toot_text += '\n' @@ -1758,7 +1535,7 @@ if __name__ == '__main__': new_game(toot_url) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word == search_new and game_waiting: @@ -1780,22 +1557,18 @@ if __name__ == '__main__': playing_with = get_locale("playing_with", player_lang1) - toot_text = '@'+username + ' ' + playing_with + ' ' + white_user + "\n" - - toot_text += '\n' + toot_text = f'@{username} {playing_with} {white_user} \n\n' player_lang2 = get_lang(white_user) your_turn = get_locale("your_turn", player_lang2) - toot_text += '@'+white_user + ': ' + your_turn + "\n" - - toot_text += '\n' + toot_text += f'@{white_user}: {your_turn}\n\n' game_name = get_locale("game_name", player_lang2) chess_hashtag = get_locale("chess_hashtag", player_lang) - toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' + toot_text += f"{game_name}: {str(game_id)} {chess_hashtag} \n" image_id = mastodon.media_post(board_file, "image/png").id @@ -1805,7 +1578,7 @@ if __name__ == '__main__': update_moves(username, game_moves) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word[:search_send_slicing] == search_send: @@ -1820,7 +1593,7 @@ if __name__ == '__main__': username_lang = get_lang(username) send_error = get_locale("send_error", username_lang) - toot_text = '@'+username + ' ' + send_error + toot_text = f'@{username} {send_error}' elif emailed == True and game_found == True: @@ -1828,7 +1601,7 @@ if __name__ == '__main__': game_number_anotations = get_locale("game_number_anotations", username_lang) anotations_sent = get_locale("anotations_sent", username_lang) - toot_text = '@'+username + ' ' + game_number_anotations + str(game_id) + ' ' + anotations_sent + toot_text = f'@{username} {game_number_anotations} {str(game_id)} {anotations_sent}' elif emailed == False and game_found == False: @@ -1837,7 +1610,7 @@ if __name__ == '__main__': username_lang = get_lang(username) cant_send_to_fediverse_account = get_locale("cant_send_to_fediverse_account", username_lang) - toot_text = '@'+username + ' ' + cant_send_to_fediverse_account + toot_text = f'@{username} {cant_send_to_fediverse_account}' else: @@ -1845,11 +1618,11 @@ if __name__ == '__main__': game_no_exists = get_locale("game_no_exists", username_lang) it_not_exists = get_locale("it_not_exists", username_lang) - toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists + toot_text = f'@{username} {game_no_exists} {str(game_id)} {it_not_exists}' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word == search_panel: @@ -1857,7 +1630,7 @@ if __name__ == '__main__': create_panel(username, rating, played_games, wins) - toot_text = '@'+username + toot_text = f'@{username}' saved_panel = 'app/panel/' + username + '_panel.png' @@ -1865,7 +1638,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility, media_ids={image_id}) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word[:4] == search_config: @@ -1873,13 +1646,13 @@ if __name__ == '__main__': lang_changed, player_lang = set_lang(account_id, username, new_lang) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) if lang_changed: locale_change_successfully = get_locale("locale_change_successfully", new_lang) - toot_text = '@'+username + ' ' + locale_change_successfully + ' ' + new_lang + toot_text = f'@{username} {locale_change_successfully} {new_lang}' mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) @@ -1887,7 +1660,7 @@ if __name__ == '__main__': locale_not_changed = get_locale("locale_not_changed", player_lang) - toot_text = '@'+username + ' ' + new_lang + ' ' + locale_not_changed + toot_text = f'@{username} {new_lang} {locale_not_changed}' mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) @@ -1895,7 +1668,7 @@ if __name__ == '__main__': toot_help() - help_text = '@'+username + help_text = f'@{username}' help_panel = 'app/panel/help_panel.png' @@ -1903,11 +1676,11 @@ if __name__ == '__main__': mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) else: - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif reply and is_playing: @@ -1917,11 +1690,11 @@ if __name__ == '__main__': game_already_started = get_locale("game_already_started", player_lang1) - toot_text = '@'+username + ' ' + game_already_started + '\n' + toot_text = f'@{username} {game_already_started} \n' if black_user != '': - toot_text += '@'+white_user + ' / ' + '@'+black_user + '\n' + toot_text += f'@{white_user} / @{black_user}\n' else: @@ -1934,7 +1707,7 @@ if __name__ == '__main__': game_name = get_locale("game_name", player_lang1) chess_hashtag = get_locale("chess_hashtag", player_lang1) - toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' + toot_text += f'{game_name}: {str(game_id)} {chess_hashtag} \n' board = chess.Board(on_going_game) @@ -1948,7 +1721,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word[:search_move_slicing] == search_move and playing_user == username: @@ -1996,11 +1769,11 @@ if __name__ == '__main__': is_not_legal_move = get_locale("is_not_legal_move", player_lang) - toot_text = '@'+username + ': ' + moving + ' ' + is_not_legal_move + '\n' + toot_text = f'@{username}: {moving} {is_not_legal_move} \n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) else: @@ -2061,7 +1834,7 @@ if __name__ == '__main__': player_lang = get_lang(playing_user) check_done = get_locale("check_done", player_lang) - toot_text = "@"+playing_user + " " + username + ' ' + check_done + '\n' + toot_text = f"@{playing_user} {username} {check_done}\n" elif check == True and checkmate == True: @@ -2071,23 +1844,23 @@ if __name__ == '__main__': check_mate_movements = get_locale("check_mate_movements", player_lang1) the_winner_is = get_locale("the_winner_is", player_lang1) - toot_text = '\n' + check_mate + ' ' + str(game_moves) + ' ' + check_mate_movements + '\n\n' + the_winner_is + ' ' + "@"+username + '\n' + toot_text = f'\n{check_mate} {str(game_moves)} {check_mate_movements}\n\n{the_winner_is} @{username}\n' winned_games = get_locale("winned_games", player_lang1) - toot_text += '\n' + winned_games + "\n" + toot_text += f'\n{winned_games}\n' rating, played_games, wins = get_stats(username) wins_of_many = get_locale("wins_of_many", player_lang1) - toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + ' (Elo: ' + str(round(rating)) + ')' + '\n' + toot_text += f'{username}: {str(wins)} {wins_of_many} {str(played_games)} (Elo: {str(round(rating))})\n' player_lang2 = get_lang(playing_user) well_done = get_locale("well_done", player_lang2) - toot_text += "\n@"+playing_user + ': ' + well_done + "\n" + toot_text += f"\n@{playing_user}: {well_done}\n" rating, played_games, wins = get_stats(playing_user) @@ -2095,25 +1868,25 @@ if __name__ == '__main__': wins_of_many = get_locale("wins_of_many", player_lang2) - toot_text += '\n\n' + winned_games + "\n" + toot_text += f'\n\n{winned_games}\n' - toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + ' (Elo: ' + str(round(rating)) + ')' + '\n' + toot_text += f'{playing_user}: {str(wins)} {wins_of_many} {str(played_games)} (Elo: {str(round(rating))})\n' elif check == False and stalemate == True: toot_text = stalemate_str + ' (' + str(game_moves) + ')' + '\n' - toot_text += '\n@'+playing_user + ', ' + '@'+username + "\n" + toot_text += f'\n@{playing_user}, @{username}\n' - toot_text += '\n' + winned_games + "\n" + toot_text += f'\n{winned_games}\n' rating, played_games, wins = get_stats(username) - toot_text += username + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + toot_text += f'{username}: {str(wins)} {wins_of_many} {str(played_games)}\n' rating, played_games, wins = get_stats(playing_user) - toot_text += playing_user + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + toot_text += f'{playing_user}: {str(wins)} {wins_of_many} {str(played_games)}\n' else: @@ -2121,20 +1894,20 @@ if __name__ == '__main__': your_turn = get_locale("your_turn", player_lang) - toot_text = '@'+playing_user + ' ' + your_turn + '\n' + toot_text = f'@{playing_user} {your_turn}\n' if capture == True and checkmate == False: player_lang = get_lang(playing_user) lost_piece = get_locale("lost_piece", player_lang) - toot_text += '\n' + lost_piece + ' ' + piece_name + '!\n' + toot_text += f'\n{lost_piece} {piece_name}!\n' game_name = get_locale("game_name", player_lang) chess_hashtag = get_locale("chess_hashtag", player_lang) - toot_text += '\n' + game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' + toot_text += f'\n{game_name}: {str(game_id)} {chess_hashtag}\n' if username == white_user: @@ -2176,7 +1949,7 @@ if __name__ == '__main__': update_moves(username, game_moves) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) except ValueError as v_error: @@ -2185,11 +1958,11 @@ if __name__ == '__main__': username_lang = get_lang(username) not_legal_move_str = get_locale("not_legal_move_str", username_lang) - toot_text = '@'+username + ' ' + not_legal_move_str + ' ' + moving + '!?)\n' + toot_text = f'@{username} {not_legal_move_str} {moving}!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) pass @@ -2200,11 +1973,11 @@ if __name__ == '__main__': username_lang = get_lang(username) not_legal_move_str = get_locale("not_legal_move_str", username_lang) - toot_text = '@'+username + ' ' + not_legal_move_str + ' ' + moving + '!?)\n' + toot_text = f'@{username} {not_legal_move_str} {moving}!?)\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) pass @@ -2220,13 +1993,13 @@ if __name__ == '__main__': player_leave_game = get_locale("player_leave_game", player_lang) - toot_text = '@'+username + ' ' + player_leave_game + ' ' + '@'+black_user + toot_text = f'@{username} {player_leave_game} @{black_user}' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) close_game(username, checkmate) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) i += 1 @@ -2236,13 +2009,13 @@ if __name__ == '__main__': player_leave_game = get_locale("player_leave_game", player_lang) - toot_text = '@'+username + ' ' + player_leave_game + ' ' + '@'+white_user + toot_text = f'@{username} {player_leave_game} @{white_user}' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) close_game(username, checkmate) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) i += 1 @@ -2252,13 +2025,13 @@ if __name__ == '__main__': leave_waiting_game = get_locale("leave_waiting_game", player_lang) - toot_text = '@'+username + ' ' + leave_waiting_game + toot_text = f'@{username} {leave_waiting_game}' mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) close_game(username, checkmate) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) i += 1 @@ -2272,7 +2045,7 @@ if __name__ == '__main__': started_games = get_locale("started_games", player_lang) - toot_text = "@"+username + ' ' + started_games + "\n" + toot_text = f"@{username} {started_games}\n" i = 0 while i < len(player1_name_lst): @@ -2281,7 +2054,7 @@ if __name__ == '__main__': game_is_waiting = get_locale("game_is_waiting", player_lang) - toot_text += '\n' + player1_name_lst[i] + ' / ' + player2_name_lst[i] + ' ' + game_is_waiting + "\n" + toot_text += f'\n{player1_name_lst[i]} / {player2_name_lst[i]} {game_is_waiting}\n' else: @@ -2289,15 +2062,15 @@ if __name__ == '__main__': if next_move_lst[i] == player1_name_lst[i]: - toot_text += '\n*' + player1_name_lst[i] + ' / ' + player2_name_lst[i] + ' ' + game_is_on_going + '\n' + toot_text += f'\n*{player1_name_lst[i]} / {player2_name_lst[i]} {game_is_on_going}\n' else: - toot_text += '\n' + player1_name_lst[i] + ' / *' + player2_name_lst[i] + ' ' + game_is_on_going + '\n' + toot_text += f'\n{player1_name_lst[i]} / *{player2_name_lst[i]} {game_is_on_going}\n' if game_link_lst[i] != None: - toot_text += str(game_link_lst[i]) + "\n" + toot_text += f'{str(game_link_lst[i])}\n' i += 1 @@ -2307,11 +2080,11 @@ if __name__ == '__main__': no_on_going_games = get_locale("no_on_going_games", player_lang) - toot_text = '@'+username + ' ' + no_on_going_games + '\n' + toot_text = f'@{username} {no_on_going_games}\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word[:search_send_slicing] == search_send: @@ -2327,25 +2100,25 @@ if __name__ == '__main__': send_error = get_locale("send_error", username_lang) - toot_text = '@'+username + ' ' + send_error + toot_text = f'@{username} {send_error}' elif emailed == True and game_found == True: game_number_anotations = get_locale("game_number_anotations", username_lang) anotations_sent = get_locale("anotations_sent", username_lang) - toot_text = '@'+username + ' ' + game_number_anotations + str(game_id) + ' ' + anotations_sent + toot_text = f'@{username} {game_number_anotations} {str(game_id)} {anotations_sent}' elif emailed == False and game_found == False: game_no_exists = get_locale("game_no_exists", username_lang) it_not_exists = get_locale("it_not_exists", username_lang) - toot_text = '@'+username + ' ' + game_no_exists + str(game_id) + ' ' + it_not_exists + toot_text = f'@{username} {game_no_exists} {str(game_id)} {it_not_exists}' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word == search_draw: @@ -2363,13 +2136,13 @@ if __name__ == '__main__': winned_games = get_locale("winned_games", player_lang1) wins_of_many = get_locale("wins_of_many", player_lang1) - toot_text = '@'+white_player + ' ' + draw_and_str + ' ' + '@'+black_player + ' ' + agreed_draw_str + '\n\n' + toot_text = f'@{white_player} {draw_and_str} @{black_player} {agreed_draw_str}\n\n' - toot_text += '\n' + winned_games + "\n" + toot_text += f'\n{winned_games}\n' rating, played_games, wins = get_stats(white_player) - toot_text += white_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + toot_text += f'{white_player}: {str(wins)} {wins_of_many} {str(played_games)}\n' player_lang2 = get_lang(black_player) draw_and_str = get_locale("draw_and_str", player_lang2) @@ -2379,13 +2152,13 @@ if __name__ == '__main__': if player_lang1 != player_lang2: - toot_text += '\n@'+white_player + ' ' + draw_and_str + ' ' + '@'+black_player + ' ' + agreed_draw_str + '\n\n' + toot_text += f'\n@{white_player} {draw_and_str} @{black_player} {agreed_draw_str}\n\n' - toot_text += '\n' + winned_games + "\n" + toot_text += f'\n{winned_games}\n' rating, played_games, wins = get_stats(black_player) - toot_text += black_player + ': ' + str(wins) + ' ' + wins_of_many + ' ' + str(played_games) + "\n" + toot_text += f'{black_player}: {str(wins)} {wins_of_many} {str(played_games)}\n' mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) @@ -2393,7 +2166,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word == search_panel: @@ -2401,7 +2174,7 @@ if __name__ == '__main__': create_panel(username, rating, played_games, wins) - toot_text = '@'+username + toot_text = f'@{username}' saved_panel = 'app/panel/' + username + '_panel.png' @@ -2409,7 +2182,7 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility, media_ids={image_id}) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) elif query_word[:4] == search_config: @@ -2417,13 +2190,13 @@ if __name__ == '__main__': lang_changed, player_lang = set_lang(account_id, username, new_lang) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) if lang_changed: locale_change_successfully = get_locale("locale_change_successfully", new_lang) - toot_text = '@'+username + ' ' + locale_change_successfully + ' ' + new_lang + toot_text = f'@{username} {locale_change_successfully} {new_lang}' mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) @@ -2431,7 +2204,7 @@ if __name__ == '__main__': locale_not_changed = get_locale("locale_not_changed", player_lang) - toot_text = '@'+username + ' ' + new_lang + ' ' + locale_not_changed + toot_text = f'@{username} {new_lang} {locale_not_changed}' mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) @@ -2439,7 +2212,7 @@ if __name__ == '__main__': toot_help() - help_text = '@'+username + help_text = f'@{username}' help_panel = 'app/panel/help_panel.png' @@ -2447,7 +2220,7 @@ if __name__ == '__main__': mastodon.status_post(help_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) else: @@ -2456,13 +2229,13 @@ if __name__ == '__main__': username_lang = get_lang(username) is_not_your_turn = get_locale("is_not_your_turn", username_lang) - toot_text = '@'+username + ' ' + is_not_your_turn + '\n' + toot_text = f'@{username} {is_not_your_turn}\n' else: is_the_turn_of = get_locale("is_the_turn_of", player_lang) - toot_text = '@'+username + ' ' + is_the_turn_of + ' ' + playing_user + "\n" + toot_text = f'@{username} {is_the_turn_of} {playing_user}\n' toot_text += '\n' @@ -2470,7 +2243,7 @@ if __name__ == '__main__': chess_hashtag = get_locale("chess_hashtag", player_lang) - toot_text += game_name + ': ' + str(game_id) + ' ' + chess_hashtag + '\n' + toot_text += f'{game_name}: {str(game_id)} {chess_hashtag}\n' board = chess.Board(on_going_game) @@ -2490,8 +2263,11 @@ if __name__ == '__main__': mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) - update_replies(status_id, username, now) + mastodon.notifications_dismiss(notification_id) + else: + + mastodon.notifications_dismiss(notification_id) i += 1 diff --git a/requirements.txt b/requirements.txt index 26a2fcf..245f95d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +wheel>=0.36.2a Mastodon.py>=1.5.1 chess>=1.3.0 psycopg2-binary>=2.8.6 -- 2.34.1