From 21242eddfa8fb963b2061683ab2280906c07c338 Mon Sep 17 00:00:00 2001 From: spla Date: Mon, 16 Nov 2020 17:54:46 +0100 Subject: [PATCH] First functional but alpha stage release --- README.md | 31 ++ db-setup.py | 174 ++++++++ mastochess.py | 1106 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 + setup.py | 238 ++++++++++ 5 files changed, 1554 insertions(+) create mode 100644 README.md create mode 100644 db-setup.py create mode 100644 mastochess.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..04aac8f --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Mastodon Chess +Play with other fediverse users a Chess game! Mastodon Chess control games, players and boards! + +Usage: + +To start a game: +@your_bot_username new + +To make a move: + +@your_bot_username move e2e4 + +### Dependencies + +- **Python 3** +- Postgresql server +- Mastodon's bot account + +### Usage: + +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. + +3. Run `python setup.py` to get your Mastodon's bot account tokens. + +4. Use your favourite scheduling method to set `python mastochess.py` to run regularly. + + diff --git a/db-setup.py b/db-setup.py new file mode 100644 index 0000000..039e407 --- /dev/null +++ b/db-setup.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import getpass +import os +import sys +from mastodon import Mastodon +from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout, MastodonAPIError +import psycopg2 +from psycopg2 import sql +from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + +# Returns the parameter from the specified file +def get_parameter( parameter, file_path ): + # Check if secrets file exists + if not os.path.isfile(file_path): + print("File %s not found, asking."%file_path) + write_parameter( parameter, file_path ) + #sys.exit(0) + + # Find parameter in file + with open( file_path ) 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) + +def write_parameter( parameter, file_path ): + if not os.path.exists('config'): + os.makedirs('config') + print("Setting up chess parameters...") + print("\n") + chess_db = input("chess db name: ") + chess_db_user = input("chess db user: ") + mastodon_db = input("Mastodon database: ") + mastodon_db_user = input("Mastodon database user: ") + + with open(file_path, "w") as text_file: + print("chess_db: {}".format(chess_db), file=text_file) + print("chess_db_user: {}".format(chess_db_user), file=text_file) + print("mastodon_db: {}".format(mastodon_db), file=text_file) + print("mastodon_db_user: {}".format(mastodon_db_user), file=text_file) + +def create_table(db, db_user, table, sql): + + conn = None + + try: + + conn = psycopg2.connect(database = db, user = db_user, password = "", host = "/var/run/postgresql", port = "5432") + cur = conn.cursor() + + print("Creating table.. "+table) + # Create the table in PostgreSQL database + cur.execute(sql) + + conn.commit() + print("Table "+table+" created!") + print("\n") + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +############################################################################### +# main + +if __name__ == '__main__': + + # Load configuration from config file + config_filepath = "config/db_config.txt" + mastodon_db = get_parameter("mastodon_db", config_filepath) + mastodon_db_user = get_parameter("mastodon_db_user", config_filepath) + chess_db = get_parameter("chess_db", config_filepath) + chess_db_user = get_parameter("chess_db_user", config_filepath) + + ############################################################ + # create database + ############################################################ + + conn = None + + try: + + conn = psycopg2.connect(dbname='postgres', + user=chess_db_user, host='', + password='') + + conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + + cur = conn.cursor() + + print("Creating database " + chess_db + ". Please wait...") + + cur.execute(sql.SQL("CREATE DATABASE {}").format( + sql.Identifier(chess_db)) + ) + print("Database " + chess_db + " created!") + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + + ############################################################ + + try: + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + # Load configuration from config file + os.remove("config/db_config.txt") + + print("Exiting. Run db-setup again with right parameters") + sys.exit(0) + + finally: + + if conn is not None: + + conn.close() + + print("\n") + print("chess parameters saved to db-config.txt!") + print("\n") + + ############################################################ + # Create needed tables + ############################################################ + + print("Creating table...") + + db = chess_db + db_user = chess_db_user + + table = "botreplies" + sql = "create table "+table+" (status_id bigint PRIMARY KEY, query_user varchar(40), status_created_at timestamptz)" + create_table(db, db_user, table, sql) + + 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, PRIMARY KEY(game_id))" + create_table(db, db_user, table, sql) + + table = "stats" + sql = "create table "+table+" (created_at timestamptz, game_id serial PRIMARY KEY, white_user varchar(40), black_user varchar(40), winner varchar(40), " + 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) + + ############################################################ + + print("Done!") + print("Now you can run setup.py!") + print("\n") diff --git a/mastochess.py b/mastochess.py new file mode 100644 index 0000000..4e0e1a9 --- /dev/null +++ b/mastochess.py @@ -0,0 +1,1106 @@ +import pdb +import sys +import os +import os.path +import re +import unidecode +from datetime import datetime, timedelta +from mastodon import Mastodon +import psycopg2 +import chess +import chess.svg +from cairosvg import svg2png + +def cleanhtml(raw_html): + cleanr = re.compile('<.*?>') + cleantext = re.sub(cleanr, '', raw_html) + return cleantext + +def unescape(s): + s = s.replace("'", "'") + return s + +def last_notification(): + + ################################################################### + # query status_created_at of last notification + + 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_created_at from botreplies order by status_created_at desc limit 1") + + row = cur.fetchone() + if row != None: + last_posted = row[0] + last_posted = last_posted.strftime("%d/%m/%Y, %H:%M:%S") + else: + last_posted = "" + + cur.close() + + return last_posted + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +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_new_notifications(): + + ############################################################################################################################# + # check if any new notifications by comparing newest notification datetime with the last query datetime + + last_notifications = [] # to store last 20 'Mention' type notitifications for our bot + + 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 * from notifications where activity_type = 'Mention' and account_id = (%s) order by created_at desc limit 1", (bot_id,)) + + row = cur.fetchone() + + if row != None: + + last_notif_created_at = row[3] + + last_notif_created_at = last_notif_created_at + timedelta(hours=2) + + last_notif_created_at = last_notif_created_at.strftime("%d/%m/%Y, %H:%M:%S") + + if last_posted != "": + + if last_notif_created_at == last_posted: + + cur.close() + + conn.close() + + print("No new notifications") + + sys.exit(0) + + cur.execute("select * from notifications where activity_type = 'Mention' and account_id = (%s) order by created_at desc limit 20", (bot_id,)) + + rows = cur.fetchall() + + if rows != None: + + for row in rows: + + last_notifications.append(row) + + cur.close() + + return last_notifications + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +def get_notification_data(): + + 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)", (user_id,)) + + row = cur.fetchone() + + if row != None: + + username = row[0] + + domain = row[1] + + cur.execute("select status_id from mentions where id = (%s)", (activity_id,)) + + row = cur.fetchone() + + if row != None: + + status_id = row[0] + + cur.execute("select text, visibility from statuses where id = (%s)", (status_id,)) + + row = cur.fetchone() + + if row != None: + + text = row[0] + + visibility = row[1] + + cur.close() + + if visibility == 0: + visibility = 'public' + elif visibility == 1: + visibility = 'unlisted' + elif visibility == 2: + visibility = 'private' + elif visibility == 3: + visibility = 'direct' + + cur.close() + + return (username, domain, status_id, text, visibility) + + 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 check_games(): + + game_id = '' + + white_user = '' + + black_user = '' + + on_going_game = '' + + waiting = False + + playing_user = '' + + try: + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + ### check if there is an ongoing game + + cur.execute("SELECT game_id, white_user, black_user, chess_game, waiting, next_move FROM games where not finished and white_user=(%s) ", (username,)) + + row = cur.fetchone() + + if row == None: + + cur.execute("SELECT game_id, white_user, black_user, chess_game, waiting, next_move FROM games where not finished and black_user=(%s)", (username,)) + + row = cur.fetchone() + + if row == None: + + is_playing = False + + else: + + is_playing = True + + game_id = row[0] + + white_user = row[1] + + if row[1] != None: + + black_user = row[2] + + else: + + black_user = '' + + on_going_game = row[3] + + waiting = row[4] + + playing_user = row[5] + + else: + + is_playing = True + + game_id = row[0] + + white_user = row[1] + + if row[2] != None: + + black_user = row[2] + + else: + + black_user = '' + + on_going_game = row[3] + + waiting = row[4] + + playing_user = row[5] + + cur.close() + + return (is_playing, game_id, white_user, black_user, on_going_game, waiting, playing_user) + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def new_game(): + + try: + + game_status = 'waiting' + + waiting = True + + board_game = board.fen() + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + insert_query = 'INSERT INTO games(created_at, white_user, chess_game, chess_status, waiting, updated_at) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING' + + cur.execute(insert_query, (now, username, board_game, game_status, waiting, now)) + + insert_query = 'INSERT INTO stats(created_at, white_user) VALUES (%s,%s) ON CONFLICT DO NOTHING' + + cur.execute(insert_query, (now, username,)) + + conn.commit() + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def update_game(board_game): + + try: + + now = datetime.now() + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("update games set chess_game=(%s), updated_at=(%s) where game_id=(%s)", (board_game, now, game_id,)) + + conn.commit() + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def close_game(): + + now = datetime.now() + + finished = True + + 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("update games set finished=(%s), updated_at=(%s) where game_id=(%s)", (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)) + + conn.commit() + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def waiting_games(): + + try: + + game_id = '' + + game_waiting = False + + 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 game_id from games where waiting order by game_id desc limit 1") + + row = cur.fetchone() + + if row != None: + + game_id = row[0] + game_waiting = True + + cur.close() + + return (game_id, game_waiting) + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def join_player(): + + try: + + now = datetime.now() + + game_status = 'waiting' + + waiting = True + + moves = 0 + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("update games set black_user=(%s), chess_status='playing', waiting='f', updated_at=(%s), moves=(%s) where game_id=(%s)", (username, now, moves, game_id,)) + + cur.execute("update stats set black_user=(%s), updated_at=(%s) where game_id=(%s)", (username, now, game_id,)) + + conn.commit() + + cur.execute("select white_user, chess_game from games where game_id=(%s)", (game_id,)) + + row = cur.fetchone() + + if row != None: + + white_user = row[0] + + chess_game = row[1] + + cur.execute("update games set next_move=(%s), updated_at=(%s) where game_id=(%s)", (white_user, now, game_id,)) + + conn.commit() + + cur.close() + + game_status = 'playing' + + return (game_status, white_user, chess_game) + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def update_moves(username, game_moves): + + try: + + now = datetime.now() + + conn = None + + conn = psycopg2.connect(database = chess_db, user = chess_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("update games set next_move=(%s), last_move=(%s), moves=(%s), updated_at=(%s) where game_id=(%s)", (playing_user, username, game_moves, now, game_id,)) + + conn.commit() + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def next_move(playing_user): + + try: + + now = datetime.now() + + waiting = True + + 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, last_move, moves from games where game_id=(%s)", (game_id,)) + + row = cur.fetchone() + + if row != None: + + white_user = row[0] + black_user = row[1] + last_move = row[2] + moves = row[3] + + if last_move != None: + + if playing_user == white_user: + + playing_user = black_user + + elif playing_user == black_user: + + playing_user = white_user + + else: + + last_move = white_user + + cur.execute("update games set next_move=(%s), updated_at=(%s) where game_id=(%s)", (playing_user, now, game_id,)) + + conn.commit() + + cur.close() + + return playing_user + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def replying(): + + moving = '' + + content = cleanhtml(text) + content = unescape(content) + + try: + + start = content.index("@") + end = content.index(" ") + if len(content) > end: + + content = content[0: start:] + content[end +1::] + + neteja = content.count('@') + + i = 0 + while i < neteja : + + start = content.rfind("@") + end = len(content) + content = content[0: start:] + content[end +1::] + i += 1 + + question = content.lower() + + query_word = question + query_word_length = len(query_word) + + if unidecode.unidecode(question)[0:query_word_length] == query_word: + + if query_word[0:4] == 'nova': + + reply = True + + elif query_word[0:3] == 'mou': + + moving = query_word[4:query_word_length].replace(" ","") + reply = True + + elif query_word[0:2] == 'fi': + + reply = True + + else: + + reply = False + + return (reply, query_word, moving) + + except ValueError as v_error: + + print(v_error) + +def mastodon(): + + # Load secrets from secrets file + secrets_filepath = "secrets/secrets.txt" + uc_client_id = get_parameter("uc_client_id", secrets_filepath) + uc_client_secret = get_parameter("uc_client_secret", secrets_filepath) + uc_access_token = get_parameter("uc_access_token", secrets_filepath) + + # Load configuration from config file + config_filepath = "config/config.txt" + mastodon_hostname = get_parameter("mastodon_hostname", config_filepath) + bot_username = get_parameter("bot_username", config_filepath) + + # Initialise Mastodon API + mastodon = Mastodon( + client_id = uc_client_id, + client_secret = uc_client_secret, + access_token = uc_access_token, + api_base_url = 'https://' + mastodon_hostname, + ) + + # Initialise access headers + headers={ 'Authorization': 'Bearer %s'%uc_access_token } + + return (mastodon, mastodon_hostname, bot_username) + +def db_config(): + + # Load db configuration from config file + config_filepath = "config/db_config.txt" + mastodon_db = get_parameter("mastodon_db", config_filepath) + mastodon_db_user = get_parameter("mastodon_db_user", config_filepath) + chess_db = get_parameter("chess_db", config_filepath) + chess_db_user = get_parameter("chess_db_user", config_filepath) + + return (mastodon_db, mastodon_db_user, chess_db, chess_db_user) + +def get_parameter( parameter, file_path ): + + if not os.path.isfile(file_path): + print("File %s not found, exiting."%file_path) + sys.exit(0) + + with open( file_path ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + print(file_path + " Missing parameter %s "%parameter) + sys.exit(0) + +def create_dir(): + if not os.path.exists('games'): + os.makedirs('games') + +def usage(): + + print('usage: python ' + sys.argv[0] + ' --play') + +############################################################################### +# main + +if __name__ == '__main__': + + # usage modes + + if len(sys.argv) == 1: + + usage() + + elif len(sys.argv) == 2: + + if sys.argv[1] == '--play': + + mastodon, mastodon_hostname, bot_username = mastodon() + + mastodon_db, mastodon_db_user, chess_db, chess_db_user = db_config() + + now = datetime.now() + + create_dir() + + last_posted = last_notification() + + bot_id = get_bot_id() + + last_notifications = get_new_notifications() + + last_notifications = sorted(last_notifications) + + #################################################################### + + i = 0 + while i < len(last_notifications): + + user_id = last_notifications[i][5] + activity_id = last_notifications[i][0] + n_created_at = last_notifications[i][3] + + n_created_at = n_created_at + timedelta(hours=2) + + n_created_datetime = n_created_at.strftime("%d/%m/%Y, %H:%M:%S") + + if n_created_datetime < last_posted: + + i +=1 + + continue + + username, domain, status_id, text, visibility = get_notification_data() + + replied = check_replies(status_id) + + if replied == True or domain != None: + + i += 1 + + continue + + if domain != None: + + update_replies(username, status_id) + + i += 1 + + # listen them or not + + reply, query_word, moving = replying() + + is_playing, game_id, white_user, black_user, on_going_game, waiting, playing_user = check_games() + + if game_id == '': + + game_id, game_waiting = waiting_games() + + if reply == True and is_playing == False: + + if query_word == 'nova' and not game_waiting: + + board = chess.Board() + + svgfile = chess.svg.board(board=board) + + board_file = 'games/' + str(game_id) + '_board.png' + + svg2png(bytestring=svgfile,write_to=board_file) + + toot_text = "@"+username+ " partida iniciada! Esperant jugador... " +"\n" + + toot_text += '\n' + + image_id = mastodon.media_post(board_file, "image/png").id + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) + + new_game() + + update_replies(status_id, username, now) + + elif query_word == 'nova' and game_waiting: + + game_status, white_user, chess_game = join_player() + + playing_user = white_user + + next_move(username) + + board = chess.Board(chess_game) + + svgfile = chess.svg.board(board=board) + + board_file = 'games/' + str(game_id) + '_board.png' + + svg2png(bytestring=svgfile,write_to=board_file) + + toot_text = "@"+username + " jugues amb " + white_user + "\n" + + toot_text += '\n' + + toot_text += "@"+white_user + ": et toca a tu" + "\n" + + toot_text += '\n' + + toot_text += '#escacs' + '\n' + + image_id = mastodon.media_post(board_file, "image/png").id + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) + + game_moves = board.ply() + + update_moves(username, game_moves) + + update_replies(status_id, username, now) + + elif reply and is_playing: + + if query_word == 'nova': + + toot_text = "@"+username + ' ja estas jugant una partida!' + '\n' + + if black_user != '': + + toot_text += '@'+white_user + ' / ' + '@'+black_user + '\n' + + else: + + toot_text += "esperant a l'altre jugador" + '\n' + + toot_text += '\n' + + toot_text += '#escacs' + '\n' + + board = chess.Board(on_going_game) + + svgfile = chess.svg.board(board=board) + + board_file = 'games/' + str(game_id) + '_board.png' + + svg2png(bytestring=svgfile,write_to=board_file) + + image_id = mastodon.media_post(board_file, "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[0:3] == 'mou' and playing_user == username: + + board = chess.Board(on_going_game) + + try: + + if chess.Move.from_uci(moving) in board.legal_moves == False: + + toot_text = "@"+username + ' moviment il·legal!' + '\n' + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + + else: + + playing_user = next_move(username) + + board.push(chess.Move.from_uci(moving)) + + if board.is_check() == True: + + toot_text = "@"+username + " t'ha fet escac!" + + if board.is_game_over() == True: + + toot_text += "\nEscac i mat! \nEl guanyador és: " + "@"+username + '\n' + + toot_text += "\n@"+playing_user + ": ben jugat!" + + close_game() + + else: + + toot_text = "@"+playing_user + ' el teu torn.'+ '\n' + + toot_text += '\n#escacs' + '\n' + + if username == white_user: + + svgfile = chess.svg.board(board=board, orientation=chess.BLACK, lastmove=chess.Move.from_uci(moving)) + + else: + + svgfile = chess.svg.board(board=board, orientation=chess.WHITE, lastmove=chess.Move.from_uci(moving)) + + board_file = 'games/' + str(game_id) + '_board.png' + + svg2png(bytestring=svgfile,write_to=board_file) + + image_id = mastodon.media_post(board_file, "image/png").id + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility, media_ids={image_id}) + + board_game = board.fen() + + update_game(board_game) + + game_moves = board.ply() + + update_moves(username, game_moves) + + update_replies(status_id, username, now) + + except ValueError as v_error: + + print(v_error) + + pass + + except AssertionError as a_error: + + print(a_error) + + toot_text = "@"+username + ' moviment il·legal!' + '\n' + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + update_replies(status_id, username, now) + + pass + + elif query_word[0:2] == 'fi': + + if black_user != '': + + if username == white_user: + + toot_text = "@"+username + " ha deixat la partida amb " + "@"+black_user + + mastodon.status_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + close_game() + + update_replies(status_id, username, now) + + i += 1 + + continue + + else: + + toot_text = "@"+username + " ha deixat la partida amb " + white_user + + mastodon.statud_post(toot_text, in_reply_to_id=status_id,visibility=visibility) + + close_game() + + update_replies(status_id, username, now) + + i += 1 + + continue + + else: + + toot_text = "@"+username + " ha abandonat la partida en espera." + + mastodon.status_post(toot_text, in_reply_to_id=status_id, visibility=visibility) + + close_game() + + update_replies(status_id, username, now) + + i += 1 + + continue + + else: + + if playing_user == None: + + toot_text = "@"+username + " no és el teu torn." + "\n" + + else: + + toot_text = "@"+username + " és el torn de " + playing_user + "\n" + + toot_text += '\n' + + toot_text += '#escacs' + '\n' + + board = chess.Board(on_going_game) + + if username == white_user: + + svgfile = chess.svg.board(board=board, orientation=chess.BLACK) + + else: + + svgfile = chess.svg.board(board=board, orientation=chess.WHITE) + + board_file = 'games/' + str(game_id) + '_board.png' + + svg2png(bytestring=svgfile,write_to=board_file) + + image_id = mastodon.media_post(board_file, "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) + + + i += 1 + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..26a2fcf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Mastodon.py>=1.5.1 +chess>=1.3.0 +psycopg2-binary>=2.8.6 +unidecode>=1.1.1 +cairosvg>=2.5.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..105383c --- /dev/null +++ b/setup.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import getpass +from mastodon import Mastodon +from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout, MastodonAPIError, MastodonIllegalArgumentError +import fileinput,re +import os +import sys + +def create_dir(): + if not os.path.exists('secrets'): + os.makedirs('secrets') + +def create_file(): + if not os.path.exists('secrets/secrets.txt'): + with open('secrets/secrets.txt', 'w'): pass + print(secrets_filepath + " created!") + +def create_config(): + if not os.path.exists('config'): + os.makedirs('config') + if not os.path.exists(config_filepath): + print(config_filepath + " created!") + with open('config/config.txt', 'w'): pass + +def create_lang_config(): + if not os.path.exists(lang_config_filepath): + print(lang_config_filepath + " created!") + with open('config/lang_config.txt', 'w'): pass + +def write_params(): + with open(secrets_filepath, 'a') as the_file: + print("Writing secrets parameter names to " + secrets_filepath) + the_file.write('uc_client_id: \n'+'uc_client_secret: \n'+'uc_access_token: \n') + +def write_config(): + with open(config_filepath, 'a') as the_file: + 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: + line = fp.readline() + cnt = 1 + while line: + if cnt == 1: + print("Writing client id to " + secrets_filepath) + modify_file(secrets_filepath, "uc_client_id: ", value=line.rstrip()) + elif cnt == 2: + print("Writing client secret to " + secrets_filepath) + modify_file(secrets_filepath, "uc_client_secret: ", value=line.rstrip()) + line = fp.readline() + cnt += 1 + +def read_token_line(self): + token_path = 'app_usercred.txt' + with open(token_path) as fp: + line = fp.readline() + print("Writing access token to " + secrets_filepath) + modify_file(secrets_filepath, "uc_access_token: ", value=line.rstrip()) + +def read_config_line(): + with open(config_filepath) as fp: + line = fp.readline() + 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: + global hostname, bot_username + hostname = input("Enter Mastodon hostname: ") + user = input("User name, ex. user@" + hostname +"? ") + user_password = getpass.getpass("User password? ") + bot_username = input("Bot's username, ex. mastochess: ") + app_name = input("This app name? ") + Mastodon.create_app(app_name, scopes=["read","write"], + to_file="app_clientcred.txt", api_base_url=hostname) + mastodon = Mastodon(client_id = "app_clientcred.txt", api_base_url = hostname) + mastodon.log_in( + user, + user_password, + scopes = ["read", "write"], + to_file = "app_usercred.txt" + ) + except MastodonIllegalArgumentError as i_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(i_error) + except MastodonNetworkError as n_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(n_error) + except MastodonReadTimeout as r_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(r_error) + except MastodonAPIError as a_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(a_error) + finally: + if error == 0: + + create_dir() + create_file() + write_params() + client_path = 'app_clientcred.txt' + read_client_lines(client_path) + token_path = 'app_usercred.txt' + read_token_line(token_path) + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt temp file..") + os.remove("app_clientcred.txt") + if os.path.exists("app_usercred.txt"): + print("Removing app_usercred.txt temp file..") + os.remove("app_usercred.txt") + print("Secrets setup done!\n") + +def modify_file(file_name,pattern,value=""): + fh=fileinput.input(file_name,inplace=True) + for line in fh: + replacement=pattern + value + line=re.sub(pattern,replacement,line) + sys.stdout.write(line) + fh.close() + +# Returns the parameter from the specified file +def get_parameter( parameter, file_path ): + # Check if secrets file exists + if not os.path.isfile(file_path): + print("File %s not found, creating it."%file_path) + log_in() + + # Find parameter in file + with open( file_path ) 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) + +# Returns the parameter from the specified file +def get_hostname( parameter, config_filepath ): + # Check if secrets file exists + if not os.path.isfile(config_filepath): + print("File %s not found, creating it."%config_filepath) + create_config() + + # Find parameter in file + with open( config_filepath ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + # Cannot find parameter, exit + print(config_filepath + " Missing parameter %s "%parameter) + write_config() + read_config_line() + 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 + +if __name__ == '__main__': + + # Load secrets from secrets file + secrets_filepath = "secrets/secrets.txt" + uc_client_id = get_parameter("uc_client_id", secrets_filepath) + uc_client_secret = get_parameter("uc_client_secret", secrets_filepath) + uc_access_token = get_parameter("uc_access_token", secrets_filepath) + + # Load configuration from config file + config_filepath = "config/config.txt" + 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 +