diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e39047a0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.ttf filter=lfs diff=lfs merge=lfs -text +*.woff filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 491bf533..015ad6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ config.ini .idea/ +.vscode/ __pycache__ diario.json libopus-0.dll music.opus opusfiles/ -ignored/* -markovmodel.json \ No newline at end of file +ignored/ +markovmodels/ +logs/ diff --git a/application.wsgi b/application.wsgi new file mode 100644 index 00000000..eeaadd0c --- /dev/null +++ b/application.wsgi @@ -0,0 +1,2 @@ +import webserver +application = webserver.app diff --git a/awardfiorygi.py b/awardfiorygi.py new file mode 100644 index 00000000..32dd33ae --- /dev/null +++ b/awardfiorygi.py @@ -0,0 +1,31 @@ +import telegram +import configparser +import db +import strings + +config = configparser.ConfigParser() +config.read("config.ini") + + +telegram_bot = telegram.Bot(config["Telegram"]["bot_token"]) +session = db.Session() + + +name = input("Utente Royalnet: ") +user = session.query(db.Royal).filter(db.Royal.username == name).one() +number = int(input("Fiorygi da aggiungere: ")) +user.fiorygi += number +reason = input("Motivazione: ") +fiorygi = f"fioryg{'i' if number != 1 else ''}" +telegram_bot.send_message(config["Telegram"]["main_group"], + strings.safely_format_string(strings.TELEGRAM.FIORYGI_AWARDED, + words={ + "mention": user.telegram[0].mention(), + "number": str(number), + "fiorygi": fiorygi, + "reason": reason + }), + parse_mode="HTML", + disable_web_page_preview=True) +session.commit() +session.close() diff --git a/cast.py b/cast.py deleted file mode 100644 index a8731bd1..00000000 --- a/cast.py +++ /dev/null @@ -1,79 +0,0 @@ -import random -import math -import db - - -def cast(spell_name: str, target_name: str, platform: str) -> str: - spell = spell_name.capitalize() - # Seed the rng with the spell name - # so that spells always deal the same damage - random.seed(spell) - dmg_dice = random.randrange(1, 11) - dmg_max = random.sample([4, 6, 8, 10, 12, 20, 100], 1)[0] - dmg_mod = random.randrange(math.floor(-dmg_max / 5), math.ceil(dmg_max / 5) + 1) - dmg_type = random.sample(["da fuoco", "da freddo", "elettrici", "sonici", "necrotici", "magici", - "da acido", "divini", "nucleari", "psichici", "fisici", "puri", "da taglio", - "da perforazione", "da impatto", "da caduta", "gelato", "onnipotenti", "oscuri", - "di luce", "da velocità", "da cactus", "meta", "dannosi", "da radiazione", - "tuamammici", "da maledizione", "pesanti", "leggeri", "immaginari", "da laser", - "da neutrini", "galattici", "cerebrali", "ritardati", "ritardanti"], 1)[0] - # Reseed the rng with a random value - # so that the dice roll always deals a different damage - random.seed() - total = dmg_mod - # Check for a critical hit - crit = 1 - while True: - crit_die = random.randrange(1, 21) - if crit_die == 20: - crit *= 2 - else: - break - for dice in range(0, dmg_dice): - total += random.randrange(1, dmg_max + 1) - if crit > 1: - if platform == "telegram": - crit_msg = f"CRITICO ×{crit}{'!' * crit}\n" - elif platform == "discord": - crit_msg = f"**CRITICO ×{crit}{'!' * crit}**\n" - total *= crit - else: - crit_msg = "" - if platform == "telegram": - if dmg_dice == 10 and dmg_max == 100 and dmg_mod == 20: - return f"❇️‼️ Ho lanciato {spell} su " \ - f"{target_name}.\n" \ - f"Una grande luce illumina il cielo, seguita poco dopo da un fungo di fumo nel luogo" \ - f" in cui si trovava {target_name}.\n" \ - f"Il fungo si espande a velocità smodata, finchè il fumo non ricopre la Terra intera e le tenebre" \ - f" cadono su di essa.\n" \ - f"Dopo qualche minuto, la temperatura ambiente raggiunge gli 0 °C, e continua a diminuire.\n" \ - f"L'Apocalisse Nucleare è giunta, e tutto per polverizzare {target_name}" \ - f" con {spell}.\n" \ - f"{target_name} subisce 10d100+20=9999 danni apocalittici!" - return f"❇️ Ho lanciato {spell} su " \ - f"{target_name}.\n" \ - f"{crit_msg}" \ - f"{target_name} subisce {dmg_dice}d{dmg_max}" \ - f"{'+' if dmg_mod > 0 else ''}{str(dmg_mod) if dmg_mod != 0 else ''}" \ - f"{'×' + str(crit) if crit > 1 else ''}" \ - f"={total if total > 0 else 0} danni {dmg_type}!" - elif platform == "discord": - if dmg_dice == 10 and dmg_max == 100 and dmg_mod == 20: - return f"❇️‼️ Ho lanciato **{spell}** su " \ - f"_{target_name}_.\n" \ - f"Una grande luce illumina il cielo, seguita poco dopo da un fungo di fumo nel luogo" \ - f" in cui si trovava _{target_name}_.\n" \ - f"Il fungo si espande a velocità smodata, finchè il fumo non ricopre la Terra intera e le tenebre" \ - f" cadono su di essa.\n" \ - f"Dopo qualche minuto, la temperatura ambiente raggiunge gli 0 °C, e continua a diminuire.\n" \ - f"L'Apocalisse Nucleare è giunta, e tutto per polverizzare _{target_name}_" \ - f" con **{spell}**.\n" \ - f"_{target_name}_ subisce 10d100+20=**9999** danni apocalittici!" - return f"❇️ Ho lanciato **{spell}** su " \ - f"_{target_name}_.\n" \ - f"{crit_msg}" \ - f"_{target_name}_ subisce {dmg_dice}d{dmg_max}" \ - f"{'+' if dmg_mod > 0 else ''}{str(dmg_mod) if dmg_mod != 0 else ''}" \ - f"{'×' + str(crit) if crit > 1 else ''}" \ - f"=**{total if total > 0 else 0}** danni {dmg_type}!" diff --git a/chatasthebot.py b/chatasthebot.py new file mode 100644 index 00000000..9e4c2c88 --- /dev/null +++ b/chatasthebot.py @@ -0,0 +1,14 @@ +import telegram +import configparser +config = configparser.ConfigParser() +config.read("config.ini") + + +telegram_bot = telegram.Bot(config["Telegram"]["bot_token"]) + + +while True: + message = input() + telegram_bot.send_message(config["Telegram"]["main_group"], message, + parse_mode="HTML", + disable_web_page_preview=True) diff --git a/db.py b/db.py index 7c326dba..d99ca514 100644 --- a/db.py +++ b/db.py @@ -1,29 +1,35 @@ import datetime import logging import os -import typing +import time + import coloredlogs from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.ext.hybrid import hybrid_property +# noinspection PyUnresolvedReferences from sqlalchemy import Column, BigInteger, Integer, String, DateTime, ForeignKey, Float, Enum, create_engine, \ UniqueConstraint, PrimaryKeyConstraint, Boolean, LargeBinary, Text, Date, func +from sqlalchemy.inspection import inspect import requests -from errors import NotFoundError, AlreadyExistingError, PrivateError +from utils.errors import NotFoundError, AlreadyExistingError, PrivateError import re import enum -# Both packages have different pip names -# noinspection PyPackageRequirements -from discord import User as DiscordUser -# noinspection PyPackageRequirements -from telegram import User as TelegramUser import loldata -from dirty import Dirty -import query_discord_music +from utils.dirty import Dirty, DirtyDelta +import sql_queries from flask import escape +import configparser +import typing +from utils import MatchmakingStatus, errors +import strings +if typing.TYPE_CHECKING: + # noinspection PyPackageRequirements + from discord import User as DiscordUser + # noinspection PyPackageRequirements + from telegram import User as TelegramUser # Init the config reader -import configparser config = configparser.ConfigParser() config.read("config.ini") @@ -38,7 +44,76 @@ os.environ["COLOREDLOGS_LOG_FORMAT"] = "%(asctime)s %(levelname)s %(name)s %(mes coloredlogs.install(level="DEBUG", logger=logger) -class Royal(Base): +def relationship_name_search(_class, keyword) -> typing.Optional[tuple]: + """Recursively find a relationship with a given name.""" + inspected = set() + + def search(_mapper, chain): + inspected.add(_mapper) + relationships = _mapper.relationships + try: + return chain + (relationships[keyword],) + except KeyError: + for _relationship in set(relationships): + if _relationship.mapper in inspected: + continue + result = search(_relationship.mapper, chain + (_relationship,)) + if result is not None: + return result + return None + + return search(inspect(_class), tuple()) + + +def relationship_link_chain(starting_class, ending_class) -> typing.Optional[tuple]: + """Find the path to follow to get from the starting table to the ending table.""" + inspected = set() + + def search(_mapper, chain): + inspected.add(_mapper) + if _mapper.class_ == ending_class: + return chain + relationships = _mapper.relationships + for _relationship in set(relationships): + if _relationship.mapper in inspected: + continue + try: + return search(_relationship.mapper, chain + (_relationship,)) + except errors.NotFoundError: + continue + raise errors.NotFoundError() + + return search(inspect(starting_class), tuple()) + + +class Mini(object): + """Mixin for every table that has an associated mini.""" + _mini_full_name = NotImplemented + _mini_name = NotImplemented + _mini_order = NotImplemented + + @classmethod + def mini_get_all(cls, session: Session) -> list: + return session.query(cls).order_by(*cls._mini_order).all() + + @classmethod + def mini_get_single(cls, session: Session, **kwargs): + return session.query(cls).filter_by(**kwargs).one_or_none() + + @classmethod + def mini_get_single_from_royal(cls, session: Session, royal: "Royal"): + chain = relationship_link_chain(cls, Royal) + if chain is None: + chain = tuple() + start = session.query(cls) + for connection in chain: + start = start.join(connection.mapper.class_) + start = start.filter(Royal.id == royal.id) + mini = start.one() + return mini + + +class Royal(Base, Mini): __tablename__ = "royals" id = Column(Integer, primary_key=True) @@ -47,6 +122,11 @@ class Royal(Base): role = Column(String) fiorygi = Column(Integer, default=0) member_since = Column(Date) + special_title = Column(String) + + _mini_full_name = "Royalnet" + _mini_name = "ryg" + _mini_order = [fiorygi.desc()] @staticmethod def create(session: Session, username: str): @@ -58,8 +138,12 @@ class Royal(Base): def __repr__(self): return f"" + @classmethod + def mini_get_single_from_royal(cls, session: Session, royal: "Royal"): + return royal -class Telegram(Base): + +class Telegram(Base, Mini): __tablename__ = "telegram" royal_id = Column(Integer, ForeignKey("royals.id")) @@ -70,8 +154,12 @@ class Telegram(Base): last_name = Column(String) username = Column(String) + _mini_full_name = "Telegram" + _mini_name = "tg" + _mini_order = [telegram_id] + @staticmethod - def create(session: Session, royal_username, telegram_user: TelegramUser): + def create(session: Session, royal_username, telegram_user: "TelegramUser"): t = session.query(Telegram).filter_by(telegram_id=telegram_user.id).first() if t is not None: raise AlreadyExistingError(repr(t)) @@ -104,8 +192,12 @@ class Telegram(Base): else: return self.first_name + @classmethod + def mini_get_single_from_royal(cls, session: Session, royal: "Royal"): + return royal.telegram -class Steam(Base): + +class Steam(Base, Mini): __tablename__ = "steam" royal_id = Column(Integer, ForeignKey("royals.id")) @@ -117,6 +209,10 @@ class Steam(Base): trade_token = Column(String) most_played_game_id = Column(BigInteger) + _mini_full_name = "Steam" + _mini_name = "steam" + _mini_order = [steam_id] + def __repr__(self): if not self.persona_name: return f"" @@ -192,8 +288,12 @@ class Steam(Base): return self.most_played_game_id = j["response"]["games"][0]["appid"] + @classmethod + def mini_get_single_from_royal(cls, session: Session, royal: "Royal"): + return royal.steam -class RocketLeague(Base): + +class RocketLeague(Base, Mini): __tablename__ = "rocketleague" steam_id = Column(String, ForeignKey("steam.steam_id"), primary_key=True) @@ -219,6 +319,13 @@ class RocketLeague(Base): wins = Column(Integer) + _mini_full_name = "Rocket League" + _mini_name = "rl" + _mini_order = [solo_std_mmr.desc().nullslast(), + doubles_mmr.desc().nullslast(), + standard_mmr.desc().nullslast(), + single_mmr.desc().nullslast()] + def __repr__(self): return f"" @@ -254,19 +361,21 @@ class RocketLeague(Base): return f"https://rocketleaguestats.com/assets/img/rocket_league/ranked/season_four/{rank}.png" -class Dota(Base): +class Dota(Base, Mini): __tablename__ = "dota" steam_id = Column(String, ForeignKey("steam.steam_id"), primary_key=True) steam = relationship("Steam", backref="dota", lazy="joined") rank_tier = Column(Integer) - wins = Column(Integer) losses = Column(Integer) - most_played_hero = Column(Integer) + _mini_full_name = "DOTA 2" + _mini_name = "dota" + _mini_order = [rank_tier.desc().nullslast(), wins.desc().nullslast()] + def __repr__(self): return f"" @@ -365,7 +474,7 @@ class RomanNumerals(enum.Enum): return self.name -class LeagueOfLegends(Base): +class LeagueOfLegends(Base, Mini): __tablename__ = "leagueoflegends" royal_id = Column(Integer, ForeignKey("royals.id")) @@ -375,7 +484,6 @@ class LeagueOfLegends(Base): summoner_id = Column(String, primary_key=True) account_id = Column(String) summoner_name = Column(String) - level = Column(Integer) solo_division = Column(Enum(LeagueOfLegendsRanks)) solo_rank = Column(Enum(RomanNumerals)) @@ -383,9 +491,17 @@ class LeagueOfLegends(Base): flex_rank = Column(Enum(RomanNumerals)) twtr_division = Column(Enum(LeagueOfLegendsRanks)) twtr_rank = Column(Enum(RomanNumerals)) - highest_mastery_champ = Column(Integer) + _mini_full_name = "League of Legends" + _mini_name = "lol" + _mini_order = [solo_division.desc().nullslast(), + solo_rank.desc().nullslast(), + flex_division.desc().nullslast(), + flex_rank.desc().nullslast(), + twtr_division.desc().nullslast(), + twtr_rank.desc().nullslast()] + def __repr__(self): if not self.summoner_name: return f"" @@ -455,14 +571,14 @@ class LeagueOfLegends(Base): def highest_mastery_champ_name(self): champ = loldata.get_champ_by_key(self.highest_mastery_champ) - return champ["name"] + return champ["id"] def highest_mastery_champ_image(self): champ = loldata.get_champ_by_key(self.highest_mastery_champ) - return loldata.get_champ_icon(champ["name"]) + return loldata.get_champ_icon(champ["id"]) -class Osu(Base): +class Osu(Base, Mini): __tablename__ = "osu" royal_id = Column(Integer, ForeignKey("royals.id"), nullable=False) @@ -470,11 +586,21 @@ class Osu(Base): osu_id = Column(Integer, primary_key=True) osu_name = Column(String) + std_pp = Column(Float, default=0) + std_best_song = Column(BigInteger) + taiko_pp = Column(Float, default=0) + taiko_best_song = Column(BigInteger) + catch_pp = Column(Float, default=0) + catch_best_song = Column(BigInteger) + mania_pp = Column(Float, default=0) + mania_best_song = Column(BigInteger) - std_pp = Column(Float) - taiko_pp = Column(Float) - catch_pp = Column(Float) - mania_pp = Column(Float) + _mini_full_name = "osu!" + _mini_name = "osu" + _mini_order = [mania_pp.desc().nullslast(), + std_pp.desc().nullslast(), + taiko_pp.desc().nullslast(), + catch_pp.desc().nullslast()] @staticmethod def create(session: Session, royal_id, osu_name): @@ -504,23 +630,32 @@ class Osu(Base): # noinspection PyUnusedLocal def update(self, session=None): + std = DirtyDelta(self.std_pp) + taiko = DirtyDelta(self.taiko_pp) + catch = DirtyDelta(self.catch_pp) + mania = DirtyDelta(self.mania_pp) r0 = requests.get(f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=0") r0.raise_for_status() + j0 = r0.json()[0] r1 = requests.get(f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=1") r1.raise_for_status() + j1 = r1.json()[0] r2 = requests.get(f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=2") r2.raise_for_status() + j2 = r2.json()[0] r3 = requests.get(f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=3") r3.raise_for_status() - j0 = r0.json()[0] - j1 = r1.json()[0] - j2 = r2.json()[0] j3 = r3.json()[0] self.osu_name = j0["username"] - self.std_pp = j0["pp_raw"] - self.taiko_pp = j1["pp_raw"] - self.catch_pp = j2["pp_raw"] - self.mania_pp = j3["pp_raw"] + std.value = float(j0["pp_raw"] or 0) + taiko.value = float(j1["pp_raw"] or 0) + catch.value = float(j2["pp_raw"] or 0) + mania.value = float(j3["pp_raw"] or 0) + self.std_pp = std.value + self.taiko_pp = taiko.value + self.catch_pp = catch.value + self.mania_pp = mania.value + return std, taiko, catch, mania def __repr__(self): if not self.osu_name: @@ -528,7 +663,7 @@ class Osu(Base): return f"" -class Discord(Base): +class Discord(Base, Mini): __tablename__ = "discord" __table_args__ = tuple(UniqueConstraint("name", "discriminator")) @@ -540,6 +675,10 @@ class Discord(Base): discriminator = Column(Integer) avatar_hex = Column(String) + _mini_full_name = "Discord" + _mini_name = "discord" + _mini_order = [discord_id] + def __str__(self): return f"{self.name}#{self.discriminator}" @@ -547,7 +686,7 @@ class Discord(Base): return f"" @staticmethod - def create(session: Session, royal_username, discord_user: DiscordUser): + def create(session: Session, royal_username, discord_user: "DiscordUser"): d = session.query(Discord).filter(Discord.discord_id == discord_user.id).first() if d is not None: raise AlreadyExistingError(repr(d)) @@ -572,8 +711,20 @@ class Discord(Base): return "https://discordapp.com/assets/6debd47ed13483642cf09e832ed0bc1b.png" return f"https://cdn.discordapp.com/avatars/{self.discord_id}/{self.avatar_hex}.png?size={size}" + @classmethod + def mini_get_all(cls, session: Session): + return [dict(row) for row in session.execute(sql_queries.all_music_query)] -class Overwatch(Base): + @classmethod + def mini_get_single(cls, session: Session, **kwargs): + return session.execute(sql_queries.one_music_query, {"royal": kwargs["royal"].id}).fetchone() + + @classmethod + def mini_get_single_from_royal(cls, session: Session, royal: "Royal"): + return cls.mini_get_single(session, royal=royal) + + +class Overwatch(Base, Mini): __tablename__ = "overwatch" royal_id = Column(Integer, ForeignKey("royals.id"), nullable=False) @@ -582,10 +733,13 @@ class Overwatch(Base): battletag = Column(String, primary_key=True) discriminator = Column(Integer, primary_key=True) icon = Column(String) - level = Column(Integer) rank = Column(Integer) + _mini_full_name = "Overwatch" + _mini_name = "ow" + _mini_order = [rank.desc().nullslast(), level.desc()] + def __str__(self, separator="#"): return f"{self.battletag}{separator}{self.discriminator}" @@ -594,45 +748,14 @@ class Overwatch(Base): @staticmethod def create(session: Session, royal_id, battletag, discriminator=None): - if discriminator is None: - battletag, discriminator = battletag.split("#", 1) - o = session.query(Overwatch).filter_by(battletag=battletag, discriminator=discriminator).first() - if o is not None: - raise AlreadyExistingError(repr(o)) - o = Overwatch(royal_id=royal_id, - battletag=battletag, - discriminator=discriminator) - o.update() - return o + raise NotImplementedError() def icon_url(self): return f"https://d1u1mce87gyfbn.cloudfront.net/game/unlocks/{self.icon}.png" # noinspection PyUnusedLocal def update(self, session=None): - r = requests.get(f"https://owapi.net/api/v3/u/{self.battletag}-{self.discriminator}/stats", headers={ - "User-Agent": "Royal-Bot/4.1", - "From": "ste.pigozzi@gmail.com" - }) - r.raise_for_status() - try: - j = r.json()["eu"]["stats"].get("competitive") - if j is None: - logger.debug(f"No stats for {repr(self)}, skipping...") - return - if not j["game_stats"]: - logger.debug(f"No stats for {repr(self)}, skipping...") - return - j = j["overall_stats"] - except TypeError: - logger.debug(f"No stats for {repr(self)}, skipping...") - return - try: - self.icon = re.search(r"https://.+\.cloudfront\.net/game/unlocks/(0x[0-9A-F]+)\.png", j["avatar"]).group(1) - except AttributeError: - logger.debug(f"No icon available for {repr(self)}.") - self.level = j["prestige"] * 100 + j["level"] - self.rank = j["comprank"] + raise NotImplementedError() def rank_url(self): if self.rank < 1500: @@ -686,48 +809,15 @@ class Diario(Base): def __str__(self): return f"{self.id} - {self.timestamp} - {self.author}: {self.text}" + def to_telegram(self): + return '#{id} di {author}\n{text}'.format( + id=self.id, + author=f"{self.author}" if self.author is not None else strings.DIARIO.ANONYMOUS, + text=escape(self.text)) + def to_html(self): return str(escape(self.text)).replace("\n", "
") - @staticmethod - def import_from_json(file): - import json - session = Session() - file = open(file, "r") - j = json.load(file) - author_ids = { - "@Steffo": 25167391, - "@GoodBalu": 19611986, - "@gattopandacorno": 200821462, - "@Albertino04": 131057096, - "@Francesco_Cuoghi": 48371848, - "@VenomousDoc": 48371848, - "@MaxSensei": 1258401, - "@Protoh": 125711787, - "@McspKap": 304117728, - "@FrankRekt": 31436195, - "@EvilBalu": 26842090, - "@Dailir": 135816455, - "@Paltri": 186843362, - "@Doom_darth_vader": 165792255, - "@httpIma": 292086686, - "@DavidoMessori": 509208316, - "@DavidoNiichan": 509208316, - "@Peraemela99": 63804599, - "@infopz": 20403805, - "@Baithoven": 121537369, - "@Tauei": 102833717 - } - for n, entry in enumerate(j): - author = author_ids[entry["sender"]] if "sender" in entry and entry["sender"] in author_ids else None - d = Diario(timestamp=datetime.datetime.fromtimestamp(float(entry["timestamp"])), - author_id=author, - text=entry["text"]) - print(f"{n} - {d}") - session.add(d) - session.commit() - session.close() - class BaluRage(Base): __tablename__ = "balurage" @@ -770,7 +860,7 @@ class VoteQuestion(Base): text = f"{self.question}\n\n" none, yes, no, abstain = 0, 0, 0, 0 if self.message_id is not None: - query = session.execute(query_discord_music.vote_answers, {"message_id": self.message_id}) + query = session.execute(sql_queries.vote_answers, {"message_id": self.message_id}) for record in query: if record["username"] == "royalgamesbot": continue @@ -837,6 +927,7 @@ class WikiEntry(Base): key = Column(String, primary_key=True) content = Column(Text, nullable=False) + locked = Column(Boolean, default=False) def __repr__(self): return f"" @@ -894,23 +985,6 @@ class Reddit(Base): return f"" -class GameLog(Base): - __tablename__ = "gamelog" - - royal_id = Column(Integer, ForeignKey("royals.id")) - royal = relationship("Royal", backref="gamelog", lazy="joined") - - username = Column(String, primary_key=True) - owned_games = Column(Integer) - unfinished_games = Column(Integer) - beaten_games = Column(Integer) - completed_games = Column(Integer) - mastered_games = Column(Integer) - - def __repr__(self): - return f"" - - class ParsedRedditPost(Base): __tablename__ = "parsedredditposts" @@ -935,7 +1009,7 @@ class LoginToken(Base): return f"" -class Halloween(Base): +class Halloween(Base, Mini): """This is some nice spaghetti, don't you think?""" __tablename__ = "halloween" @@ -943,7 +1017,6 @@ class Halloween(Base): royal = relationship("Royal", backref="halloween", lazy="joined") first_trigger = Column(DateTime) - puzzle_piece_a = Column(DateTime) puzzle_piece_b = Column(DateTime) puzzle_piece_c = Column(DateTime) @@ -951,9 +1024,12 @@ class Halloween(Base): puzzle_piece_e = Column(DateTime) puzzle_piece_f = Column(DateTime) puzzle_piece_g = Column(DateTime) - boss_battle = Column(DateTime) + _mini_full_name = "Halloween 2018" + _mini_name = "halloween2018" + _mini_order = [first_trigger] + def __getitem__(self, item): if not isinstance(item, int): raise TypeError("The index should be an int") @@ -1036,6 +1112,267 @@ class ActivityReport(Base): return f"" +class Quest(Base): + __tablename__ = "quests" + + id = Column(Integer, primary_key=True) + + title = Column(String) + description = Column(Text) + reward = Column(Integer) + expiration_date = Column(DateTime) + + def __repr__(self): + return f"" + + +class Terraria13(Base, Mini): + __tablename__ = "terraria13" + + game_name = "Terraria 13" + + royal_id = Column(Integer, ForeignKey("royals.id"), primary_key=True) + royal = relationship("Royal", backref="terraria13", lazy="joined") + + character_name = Column(String) + contribution = Column(Integer) + + _mini_full_name = "Terraria 13" + _mini_name = "terraria13" + _mini_order = [contribution.desc()] + + def __repr__(self): + return f"" + + +class Minecraft2019(Base, Mini): + __tablename__ = "minecraft2019" + + game_name = "Minecraft 2019" + + royal_id = Column(Integer, ForeignKey("royals.id"), primary_key=True) + royal = relationship("Royal", backref="minecraft2019", lazy="joined") + + character_name = Column(String) + contribution = Column(Integer) + + _mini_full_name = "Minecraft 2019" + _mini_name = "minecraft2019" + _mini_order = [contribution.desc()] + + def __repr__(self): + return f"" + + +mini_list = [Royal, Telegram, Steam, Dota, LeagueOfLegends, Osu, Discord, Overwatch, Halloween, + Terraria13] + + +class Match(Base): + __tablename__ = "matches" + + id = Column(Integer, primary_key=True) + timestamp = Column(DateTime) + creator_id = Column(BigInteger, ForeignKey("telegram.telegram_id")) + creator = relationship("Telegram", backref="matches_created", lazy="joined") + + match_title = Column(String) + match_desc = Column(Text) + min_players = Column(Integer) + max_players = Column(Integer) + closed = Column(Boolean, default=False) + + message_id = Column(BigInteger) + + def active_players_count(self): + count = 0 + for player in self.players: + if player.status == MatchmakingStatus.READY \ + or player.status == MatchmakingStatus.WAIT_FOR_ME \ + or player.status == MatchmakingStatus.SOMEONE_ELSE: + count += 1 + return count + + def generate_text(self, session): + player_list = session.query(MatchPartecipation).filter_by(match=self).all() + title = f"{self.match_title}" + description = f"{self.match_desc}\n" if self.match_desc else "" + if self.min_players: + minimum = f" (minimo {self.min_players})" + else: + minimum = "" + plist = f"Giocatori{minimum}:\n" + ignore_count = 0 + for player in player_list: + icon = strings.MATCHMAKING.ENUM_TO_EMOJIS[player.status] + if player.status == MatchmakingStatus.IGNORED: + ignore_count += 1 + continue + plist += f"{icon} {player.user.royal.username}\n" + if ignore_count: + ignored = f"❌ {ignore_count} persone non sono interessate.\n" + else: + ignored = "" + if self.max_players: + players = f"[{self.active_players_count()}/{self.max_players}]" + else: + players = f"[{self.active_players_count()}]" + close = f"[matchmaking terminato]\n" if self.closed else "" + message = f"{title} {players}\n" \ + f"{description}\n" \ + f"{plist}\n" \ + f"{ignored}" \ + f"{close}" + return message + + def __repr__(self): + return f"" + + def format_dict(self) -> typing.Dict[str, str]: + return { + "id": str(self.id), + "timestamp": self.timestamp.isoformat(), + "creator_id": str(self.creator_id), + "creator_name": self.creator.mention(), + "match_title": self.match_title, + "match_desc": self.match_desc if self.match_desc is not None else "", + "min_players": str(self.min_players) if self.min_players is not None else "", + "max_players": str(self.max_players) if self.max_players is not None else "", + "active_players": str(self.active_players_count()), + "players": str(len(self.players)) + } + + +class MatchPartecipation(Base): + __tablename__ = "matchpartecipations" + __table_args__ = (PrimaryKeyConstraint("user_id", "match_id"),) + + user_id = Column(BigInteger, ForeignKey("telegram.telegram_id")) + user = relationship("Telegram", backref="match_partecipations", lazy="joined") + + match_id = Column(Integer, ForeignKey("matches.id")) + match = relationship("Match", backref="players", lazy="joined") + + status = Column(Integer) + + def __repr__(self): + return f"" + + +class BindingOfIsaac(Base, Mini): + __tablename__ = "bindingofisaac" + + steam_id = Column(String, ForeignKey("steam.steam_id"), primary_key=True) + steam = relationship("Steam", backref="binding_of_isaac", lazy="joined") + + daily_victories = Column(Integer, default=0) + + def __repr__(self): + return f"" + + def recalc_victories(self): + raise NotImplementedError() # TODO + + +class BindingOfIsaacRun(Base): + __tablename__ = "bindingofisaacruns" + __table_args__ = (PrimaryKeyConstraint("date", "player_id"),) + + date = Column(Date) + + player_id = Column(String, ForeignKey("bindingofisaac.steam_id")) + player = relationship("BindingOfIsaac", backref="runs", lazy="joined") + + score = Column(BigInteger) + # time = Column(???) + + def __repr__(self): + return f"" + + +class Brawlhalla(Base, Mini): + __tablename__ = "brawlhalla" + + steam_id = Column(String, ForeignKey("steam.steam_id"), primary_key=True) + steam = relationship("Steam", backref="brawlhalla", lazy="joined") + brawlhalla_id = Column(BigInteger) + name = Column(String) + + level = Column(Integer) + + main_legend_name = Column(String) + main_legend_level = Column(Integer) + + ranked_plays = Column(Integer) + ranked_wins = Column(Integer) + rating = Column(Integer) + + best_team_partner_id = Column(BigInteger) + best_team_rating = Column(Integer) + + _mini_full_name = "Brawlhalla" + _mini_name = "brawlhalla" + _mini_order = [rating.desc(), level.desc()] + + def __repr__(self): + return f"" + + @hybrid_property + def best_team_partner(self) -> typing.Optional["Brawlhalla"]: + # FIXME: dirty hack here + session = Session() + b = session.query(Brawlhalla).filter_by(brawlhalla_id=self.best_team_partner_id).one_or_none() + session.close() + return b + + @staticmethod + def init_table(): + session = Session() + steam = session.query(Steam).all() + for user in steam: + j = requests.get("https://api.brawlhalla.com/search", params={ + "steamid": user.steam_id, + "api_key": config["Brawlhalla"]["brawlhalla_api_key"] + }).json() + if not j: + continue + b = session.query(Brawlhalla).filter_by(steam=user).one_or_none() + if b is None: + b = Brawlhalla(steam_id=user.steam_id, brawlhalla_id=j["brawlhalla_id"], name=j["name"]) + session.add(b) + time.sleep(1) + session.commit() + + def update(self, session=None): + j = requests.get(f"https://api.brawlhalla.com/player/{self.brawlhalla_id}/stats?api_key={config['Brawlhalla']['brawlhalla_api_key']}").json() + self.name = j.get("name", "unknown") + self.level = j.get("level", 0) + try: + main_legend = max(j.get("legends", []), key=lambda l: l.get("level", 0)) + self.main_legend_level = main_legend.get("level", 0) + self.main_legend_name = main_legend.get("legend_name_key", "unknown") + except ValueError: + pass + j = requests.get(f"https://api.brawlhalla.com/player/{self.brawlhalla_id}/ranked?api_key={config['Brawlhalla']['brawlhalla_api_key']}").json() + self.ranked_plays = j.get("games", 0) + self.ranked_wins = j.get("wins", 0) + rating = DirtyDelta(self.rating) + rating.value = j.get("rating") + self.rating = rating.value + best_team_data = Dirty((self.best_team_partner_id, self.best_team_rating)) + try: + current_best_team = max(j.get("2v2", []), key=lambda t: t.get("rating", 0)) + if current_best_team["brawlhalla_id_one"] == self.brawlhalla_id: + self.best_team_partner_id = current_best_team["brawlhalla_id_two"] + else: + self.best_team_partner_id = current_best_team["brawlhalla_id_one"] + self.best_team_rating = current_best_team["rating"] + best_team_data.value = (self.best_team_partner_id, self.best_team_rating) + except ValueError: + pass + return rating, best_team_data + + # If run as script, create all the tables in the db if __name__ == "__main__": print("Creating new tables...") diff --git a/dirty.py b/dirty.py deleted file mode 100644 index 9a95ede8..00000000 --- a/dirty.py +++ /dev/null @@ -1,13 +0,0 @@ -class Dirty: - def __init__(self, initial_value): - self.initial_value = initial_value - self.value = initial_value - - def is_clean(self): - return self.initial_value == self.value - - def is_dirty(self): - return not self.is_clean() - - def __bool__(self): - return self.is_dirty() diff --git a/isaacfetcher.py b/isaacfetcher.py new file mode 100644 index 00000000..e35ae842 --- /dev/null +++ b/isaacfetcher.py @@ -0,0 +1,55 @@ +import steamleaderboards +import datetime +import db +import time +import telegram +import configparser + + +def dates_generator(last_date: datetime.date): + date = datetime.date.today() + while True: + if date < last_date: + return + yield date + date -= datetime.timedelta(days=1) + + +session = db.Session() +players = session.query(db.BindingOfIsaac).all() + +config = configparser.ConfigParser() +config.read("config.ini") + +print("Fetching leaderboardgroup...") +isaac = steamleaderboards.LeaderboardGroup(250900) + +telegram_bot = telegram.Bot(config["Telegram"]["bot_token"]) + +for date in dates_generator(datetime.date(year=2017, month=1, day=3)): + lb_name = "{year:04d}{month:02d}{day:02d}_scores+".format(year=date.year, month=date.month, day=date.day) + print(f"Fetching {lb_name}...") + leaderboard = isaac.get(name=lb_name) + print(f"Finding players...") + runs = [] + for player in players: + entry = leaderboard.find_entry(player.steam_id) + if entry is None: + continue + print(f"Found new entry: {entry}") + run = db.BindingOfIsaacRun(player=player, score=entry.score, date=date) + runs.append(run) + session.add(run) + if len(runs) > 1: + runs.sort(key=lambda x: x.score) + best = runs[-1] + best.player.daily_victories += 1 + try: + telegram_bot.send_message(config["Telegram"]["main_group"], + f"🏆 {best.player.steam.persona_name} ha vinto la Daily Run di Isaac del {date.isoformat()}!", + parse_mode="HTML", disable_web_page_preview=True, disable_notification=True) + except Exception: + pass + session.commit() + print("Sleeping 5s...") + time.sleep(5) diff --git a/loldata.py b/loldata.py index 1d5faf0d..cba0ef61 100644 --- a/loldata.py +++ b/loldata.py @@ -1,4 +1,4 @@ -import errors +from utils import errors champions = {"type": "champion", "format": "standAloneComplex", "version": "8.19.1", "data": { "Aatrox": {"version": "8.19.1", "id": "Aatrox", "key": "266", "name": "Aatrox", "title": "the Darkin Blade", diff --git a/newuser.py b/newuser.py index 334cc500..ed283997 100644 --- a/newuser.py +++ b/newuser.py @@ -6,7 +6,7 @@ user = session.query(db.Royal).filter_by(username=username).one_or_none() if user is None: user = db.Royal.create(session, username) session.add(user) -session.commit() +session.flush() try: steam = db.Steam.create(session, user.id, input("Steam ID 1: ")) except Exception as e: diff --git a/requirements.txt b/requirements.txt index dea73f34..f23d2966 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ python-telegram-bot flask sqlalchemy -youtube-dl +youtube_dl==2019.02.18 requests psycopg2-binary PyNaCl @@ -15,4 +15,6 @@ praw dice raven[flask] coloredlogs -sentry_sdk \ No newline at end of file +sentry_sdk +steamleaderboards +git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice] diff --git a/query_discord_music.py b/sql_queries.py similarity index 87% rename from query_discord_music.py rename to sql_queries.py index 3e57d21e..aa7b29cc 100644 --- a/query_discord_music.py +++ b/sql_queries.py @@ -1,4 +1,4 @@ -all_query = """SELECT +all_music_query = """SELECT discord.royal_id, discord.discord_id, discord.name, @@ -59,7 +59,7 @@ LEFT JOIN # TODO: can and should be optimized, but I'm too lazy for that -one_query = """SELECT +one_music_query = """SELECT discord.royal_id, discord.discord_id, discord.name, @@ -141,3 +141,18 @@ LEFT JOIN WHERE votequestion.message_id = :message_id ) answer ON telegram.telegram_id = answer.user_id ORDER BY answer.choice;""" + +activity_by_hour = """SELECT AVG(discord_members_online) online_members_avg, + AVG(discord_members_ingame) ingame_members_avg, + AVG(discord_members_cv) cv_members_avg, + AVG(discord_channels_used) channels_used_avg, + AVG(discord_cv) cv_avg, + extract(hour from timestamp) h +FROM ( + SELECT *, + extract(month from timestamp) month_ + FROM activityreports + ) withmonth +WHERE withmonth.month_ = :current_month +GROUP BY h +ORDER BY h;""" \ No newline at end of file diff --git a/stagismo.py b/stagismo.py deleted file mode 100644 index b49340e6..00000000 --- a/stagismo.py +++ /dev/null @@ -1,46 +0,0 @@ -listona = ["della secca", "del seccatore", "del secchiello", "del secchio", "del secchione", "del secondino", - "del sedano", "del sedativo", "della sedia", "del sedicente", "del sedile", "della sega", "del segale", - "della segatura", "della seggiola", "del seggiolino", "della seggiovia", "della segheria", "del seghetto", - "del segnalibro", "del segnaposto", "del segno", "del segretario", "della segreteria", "del seguace", - "del segugio", "della selce", "della sella", "della selz", "della selva", "della selvaggina", "del semaforo", - "del seme", "del semifreddo", "del seminario", "della seminarista", "della semola", "del semolino", - "del semplicione", "della senape", "del senatore", "del seno", "del sensore", "della sentenza", - "della sentinella", "del sentore", "della seppia", "del sequestratore", "della serenata", "del sergente", - "del sermone", "della serpe", "del serpente", "della serpentina", "della serra", "del serraglio", - "del serramanico", "della serranda", "della serratura", "del servitore", "della servitù", "del servizievole", - "del servo", "del set", "della seta", "della setola", "del sidecar", "del siderurgico", "del sidro", - "della siepe", "del sifone", "della sigaretta", "del sigaro", "del sigillo", "della signora", - "della signorina", "del silenziatore", "della silhouette", "del silicio", "del silicone", "del siluro", - "della sinagoga", "della sindacalista", "del sindacato", "del sindaco", "della sindrome", "della sinfonia", - "del sipario", "del sire", "della sirena", "della siringa", "del sismografo", "del sobborgo", - "del sobillatore", "del sobrio", "del soccorritore", "del socio", "del sociologo", "della soda", "del sofà", - "della soffitta", "del software", "dello sogghignare", "del soggiorno", "della sogliola", "del sognatore", - "della soia", "del solaio", "del solco", "del soldato", "del soldo", "del sole", "della soletta", - "della solista", "del solitario", "del sollazzare", "del sollazzo", "del sollecito", "del solleone", - "del solletico", "del sollevare", "del sollievo", "del solstizio", "del solubile", "del solvente", - "della soluzione", "del somaro", "del sombrero", "del sommergibile", "del sommo", "della sommossa", - "del sommozzatore", "del sonar", "della sonda", "del sondaggio", "del sondare", "del sonnacchioso", - "del sonnambulo", "del sonnellino", "del sonnifero", "del sonno", "della sonnolenza", "del sontuoso", - "del soppalco", "del soprabito", "del sopracciglio", "del sopraffare", "del sopraffino", "del sopraluogo", - "del sopramobile", "del soprannome", "del soprano", "del soprappensiero", "del soprassalto", - "del soprassedere", "del sopravvento", "del sopravvivere", "del soqquadro", "del sorbetto", "del sordido", - "della sordina", "del sordo", "della sorella", "della sorgente", "del sornione", "del sorpasso", - "della sorpresa", "del sorreggere", "del sorridere", "della sorsata", "del sorteggio", "del sortilegio", - "del sorvegliante", "del sorvolare", "del sosia", "del sospettoso", "del sospirare", "della sosta", - "della sostanza", "del sostegno", "del sostenitore", "del sostituto", "del sottaceto", "della sottana", - "del sotterfugio", "del sotterraneo", "del sottile", "del sottilizzare", "del sottintendere", - "del sottobanco", "del sottobosco", "del sottomarino", "del sottopassaggio", "del sottoposto", - "del sottoscala", "della sottoscrizione", "del sottostare", "del sottosuolo", "del sottotetto", - "del sottotitolo", "del sottovalutare", "del sottovaso", "della sottoveste", "del sottovuoto", - "del sottufficiale", "della soubrette", "del souvenir", "del soverchiare", "del sovrano", "del sovrapprezzo", - "della sovvenzione", "del sovversivo", "del sozzo", "dello suadente", "del sub", "del subalterno", - "del subbuglio", "del subdolo", "del sublime", "del suburbano", "del successore", "del succo", - "della succube", "del succulento", "della succursale", "del sudario", "della sudditanza", "del suddito", - "del sudicio", "del suffisso", "del suffragio", "del suffumigio", "del suggeritore", "del sughero", - "del sugo", "del suino", "della suite", "del sulfureo", "del sultano", "di Steffo", "di Spaggia", - "di Sabrina", "del sas", "del ses", "del sis", "del sos", "del sus", "della supremazia", "del Santissimo", - "della scatola", "del supercalifragilistichespiralidoso", "del sale", "del salame", "di (Town of) Salem", - "di Stronghold", "di SOMA", "dei Saints", "di S.T.A.L.K.E.R.", "di Sanctum", "dei Sims", "di Sid", - "delle Skullgirls", "di Sonic", "di Spiral (Knights)", "di Spore", "di Starbound", "di SimCity", "di Sensei", - "di Ssssssssssssss... Boom! E' esploso il dizionario", "della scala", "di Sakura", "di Suzie", "di Shinji", - "del senpai", "del support", "di Superman"] diff --git a/static/FixMeRYGLogo.jpg b/static/FixMeRYGLogo.jpg deleted file mode 100644 index de56fa61..00000000 Binary files a/static/FixMeRYGLogo.jpg and /dev/null differ diff --git a/static/LogoRoyalGames.png b/static/LogoRoyalGames.png new file mode 100644 index 00000000..6ea92045 --- /dev/null +++ b/static/LogoRoyalGames.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dea386e1d6ef2f0004aa045385f37f559247d9f5a17010174b4be5b6465dfeb6 +size 50018 diff --git a/static/LogoRoyalGames.svg b/static/LogoRoyalGames.svg new file mode 100644 index 00000000..373effee --- /dev/null +++ b/static/LogoRoyalGames.svg @@ -0,0 +1,219 @@ + + + + + Royal Games + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Royal Games + + + + + + + + diff --git a/static/LogoRoyalGamesDota.png b/static/LogoRoyalGamesDota.png new file mode 100644 index 00000000..c7140bc4 --- /dev/null +++ b/static/LogoRoyalGamesDota.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d91f90f19b96654c753f515c19f4191ad9881cefec249ac4decb5f872d344049 +size 6499 diff --git a/static/LogoRoyalGamesDota.svg b/static/LogoRoyalGamesDota.svg new file mode 100644 index 00000000..16e762a0 --- /dev/null +++ b/static/LogoRoyalGamesDota.svg @@ -0,0 +1,219 @@ + + + + + Royal Games + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Royal Games + + + + + + + + diff --git a/static/LogoRoyalGamesDotaBanner.png b/static/LogoRoyalGamesDotaBanner.png new file mode 100644 index 00000000..c8d7b908 --- /dev/null +++ b/static/LogoRoyalGamesDotaBanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af8a851565bba2e4a20bb0ad0492249941c220d1b3e6d6f480406f9488728e50 +size 11441 diff --git a/static/LogoRoyalGamesDotaBanner.svg b/static/LogoRoyalGamesDotaBanner.svg new file mode 100644 index 00000000..dedff800 --- /dev/null +++ b/static/LogoRoyalGamesDotaBanner.svg @@ -0,0 +1,346 @@ + + + + + Royal Games + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Royal Games + + + + + + + + + + + + + + + + + + diff --git a/static/beaufortlol.woff b/static/beaufortlol.woff index 59ecd6e6..c68de1fd 100644 Binary files a/static/beaufortlol.woff and b/static/beaufortlol.woff differ diff --git a/static/lol-rank-icons/bronze.png b/static/lol-rank-icons/bronze.png index ca153705..ff62b120 100644 Binary files a/static/lol-rank-icons/bronze.png and b/static/lol-rank-icons/bronze.png differ diff --git a/static/lol-rank-icons/challenger.png b/static/lol-rank-icons/challenger.png index 482a078c..0fc9ec28 100644 Binary files a/static/lol-rank-icons/challenger.png and b/static/lol-rank-icons/challenger.png differ diff --git a/static/lol-rank-icons/diamond.png b/static/lol-rank-icons/diamond.png index e8e8ac76..ea41f948 100644 Binary files a/static/lol-rank-icons/diamond.png and b/static/lol-rank-icons/diamond.png differ diff --git a/static/lol-rank-icons/gold.png b/static/lol-rank-icons/gold.png index 26d0d475..21338e33 100644 Binary files a/static/lol-rank-icons/gold.png and b/static/lol-rank-icons/gold.png differ diff --git a/static/lol-rank-icons/grandmaster.png b/static/lol-rank-icons/grandmaster.png index c789e92a..9bec767a 100644 Binary files a/static/lol-rank-icons/grandmaster.png and b/static/lol-rank-icons/grandmaster.png differ diff --git a/static/lol-rank-icons/iron.png b/static/lol-rank-icons/iron.png index d7964089..f78df5a6 100644 Binary files a/static/lol-rank-icons/iron.png and b/static/lol-rank-icons/iron.png differ diff --git a/static/lol-rank-icons/master.png b/static/lol-rank-icons/master.png index 3882eea8..64cba1da 100644 Binary files a/static/lol-rank-icons/master.png and b/static/lol-rank-icons/master.png differ diff --git a/static/lol-rank-icons/platinum.png b/static/lol-rank-icons/platinum.png index ce7a2971..dc64a147 100644 Binary files a/static/lol-rank-icons/platinum.png and b/static/lol-rank-icons/platinum.png differ diff --git a/static/lol-rank-icons/provisional.png b/static/lol-rank-icons/provisional.png index cc76f50a..680c1164 100644 Binary files a/static/lol-rank-icons/provisional.png and b/static/lol-rank-icons/provisional.png differ diff --git a/static/lol-rank-icons/silver.png b/static/lol-rank-icons/silver.png index 8503016e..c5236853 100644 Binary files a/static/lol-rank-icons/silver.png and b/static/lol-rank-icons/silver.png differ diff --git a/static/nryg.less b/static/nryg.less index 5ae7619a..c5bfee03 100644 --- a/static/nryg.less +++ b/static/nryg.less @@ -18,10 +18,6 @@ h1, h2, h3, h4, h5, h6 { color: @accent-color; } -h1 .whatsthis { - font-size: large; -} - a { color: @link-color; text-decoration: none; @@ -75,7 +71,7 @@ code { font-family: "Consolas", "Source Code Pro", monospace; } -input[type="text"], input[type="password"] { +input[type="text"], input[type="password"], input[type="email"] { background-color: rgba(red(@text-color), green(@text-color), blue(@text-color), 0.1); color: @text-color; border: none; @@ -213,21 +209,60 @@ table { } } +ul { + list-style: none; + + &.multicolumn { + column-width: 192px; + word-wrap: break-word; + } + + li { + &::before { + content: "•"; + display: inline-block; + width: 1em; + margin-left: -1em; + } + + &.list-admin { + &::before { + color: #ffff00; + } + } + + &.list-locked { + &::before { + color: #555555; + } + } + } +} + +img { + max-width: 100%; + + @media (max-width:792px) { + max-width: initial; + } +} + +.gravatar { + border-radius: 16px; + width: 32px; + height: 32px; + vertical-align: middle; +} + +.whatsthis { + font-size: large; +} + .omnicontainer { - width: 380px; + width: 100%; margin-left: auto; margin-right: auto; - @media (min-width: 792px) - { - width: 776px; - } - - @media (min-width: 1180px) - { - width: 1164px; - } - @media (min-width: 1568px) { width: 1552px; } @@ -270,18 +305,6 @@ table { } } -@media (min-width:601px) { - .mobile-only { - display: none; - } -} - -@media (max-width:600px) { - .desktop-only { - display: none; - } -} - .game-panel { display: inline-block; margin: 4px; @@ -511,6 +534,10 @@ table { img.medal { height: 60px; + + &.locked { + opacity: 0.3; + } } } } @@ -696,30 +723,78 @@ table { .ryg { background-color: rgba(red(@text-color), green(@text-color), blue(@text-color), 0.1); padding: 18px; - grid-template-columns: 80% 20%; + grid-template-rows: 32px 16px 16px 36px; + grid-template-columns: 33.3% 33.3% 33.3%; - .player { + .username { grid-row: 1; - grid-column-start: 1; - grid-column-end: 3; - font-weight: bold; + grid-column: 1 / 3; + font-size: x-large; + align-self: center; + justify-self: flex-start; } - .member-status { + .special-title { + grid-row: 1; + grid-column: 3; + font-size: small; + align-self: center; + justify-self: flex-end; + font-style: italic; + color: @accent-color; + } + + .join-date-title { + grid-row: 3; grid-column: 1; - grid-row-start: 2; - grid-row-end: 4; - font-size: x-large; - margin-top: auto; - margin-bottom: auto; + font-size: xx-small; + align-self: center; + justify-self: center; + } + + .join-date { + grid-row: 4; + grid-column: 1; + font-size: large; + align-self: center; + justify-self: center; + + } + + .wiki-edits-title { + grid-row: 3; + grid-column: 2; + font-size: xx-small; + align-self: center; + justify-self: center; + } + + .wiki-edits { + grid-row: 4; + grid-column: 2; + font-size: large; + align-self: center; + justify-self: center; + } + + .fiorygi-title { + grid-row: 3; + grid-column: 3; + font-size: xx-small; + align-self: center; + justify-self: center; } .fiorygi { - grid-column: 2; + grid-row: 4; + grid-column: 3; + font-size: large; + align-self: center; + justify-self: center; } - .fiorygi.game-score { - font-size: x-large; + .unknown { + opacity: 0.3; } } @@ -809,13 +884,50 @@ table { } } } + + .terraria13 { + border: 3px solid black; + padding: 15px; + background-image: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url("/static/stoneslab.png"); + font-family: "Andy", sans-serif; + font-weight: bold; + color: white; + grid-row-gap: 5px; + + .character { + grid-row: 1; + font-size: x-large; + } + + .contribution-status { + grid-row: 2; + + .contribution { + font-size: xx-large; + + &.blue { + color: #8686e5; + } + + &.green { + color: #92f892; + } + + &.orange { + color: #e9b688; + } + } + } + } } .wiki { - .wiki-log { + .wiki-log, .wiki-locked { font-family: "Consolas", "Source Code Pro", monospace; + font-size: small; margin-bottom: 12px; + opacity: 0.4; .last-reason { font-style: italic; @@ -837,7 +949,7 @@ table { } } -.entry { +.diario { display: grid; .left { @@ -854,15 +966,15 @@ table { font-size: smaller; } - .entry-id { + .diario-id { text-align: right; } } -.main-page { +.two-columns { display: grid; - @media (min-width:601px) { + @media (min-width:791px) { //Desktop grid-template-columns: 50% 50%; @@ -876,7 +988,7 @@ table { } } - @media (max-width:600px) { + @media (max-width:792px) { //Mobile grid-template-columns: 100%; @@ -894,11 +1006,6 @@ table { grid-row-end: 3; } } - - ul { - column-width: 200px; - word-wrap: break-word; - } } .event { @@ -942,6 +1049,65 @@ table { } } +.quest { + padding: 8px; + color: @accent-color; + font-family: "Verdana", sans-serif; + background-image: url("/static/plank.jpg"); + display: grid; + grid-template-columns: auto 64px; + grid-row-gap: 2px; + margin-bottom: 1px; + margin-top: 1px; + border-radius: 4px; + + .title { + grid-row: 1; + grid-column: 1; + font-weight: bold; + } + + .description { + grid-row: 2; + grid-column: 1; + + p { + margin: 0; + } + } + + .reward { + grid-row-start: 1; + grid-row-end: 3; + grid-column: 2; + text-align: center; + display: flex; + flex-direction: column; + + .number { + font-size: xx-large; + } + + .subtext { + font-size: smaller; + } + } +} + +#royal-games { + font-size: 64px; + text-align: center; + + @media (max-width:792px) { + font-size: 32px; + } + + img { + height: 128px; + vertical-align: middle; + } +} + #debug-mode { color: red; font-weight: bold; @@ -950,4 +1116,4 @@ table { #edit-css { font-size: medium; -} +} \ No newline at end of file diff --git a/static/plank.jpg b/static/plank.jpg new file mode 100644 index 00000000..2b2e7df1 --- /dev/null +++ b/static/plank.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec59480cf8db818b449242d83ec720ae23154eff9f8cdf33ac6c2a53bfb4f3aa +size 1459923 diff --git a/static/radiancedota.ttf b/static/radiancedota.ttf index 6251d2e5..db824d26 100644 Binary files a/static/radiancedota.ttf and b/static/radiancedota.ttf differ diff --git a/static/rank_icon_locked.png b/static/rank_icon_locked.png new file mode 100644 index 00000000..9db86fac --- /dev/null +++ b/static/rank_icon_locked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebabd07ca06e02e43486019112ffaa65cb068aa15340797d5c46980c32a6116f +size 37273 diff --git a/static/snesfile.zip b/static/snesfile.zip deleted file mode 100644 index ee6b13f5..00000000 Binary files a/static/snesfile.zip and /dev/null differ diff --git a/static/stoneslab.png b/static/stoneslab.png new file mode 100644 index 00000000..3fa34a14 --- /dev/null +++ b/static/stoneslab.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ddb8b02a67b7890df9530ffe7ccc1c1b2fc700045b2ffd483694bbdffd561f0 +size 12108 diff --git a/static/terrariafont.ttf b/static/terrariafont.ttf new file mode 100644 index 00000000..b381990d --- /dev/null +++ b/static/terrariafont.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7697ec2286230bcfbb4c668968193d0da1621e7b97bd15137e51811b482eaf2d +size 83083 diff --git a/static/whitney.woff b/static/whitney.woff index 98aa2395..85e05f0b 100644 Binary files a/static/whitney.woff and b/static/whitney.woff differ diff --git a/statsupdate.py b/statsupdate.py index 2dd0ef6a..c505eec2 100644 --- a/statsupdate.py +++ b/statsupdate.py @@ -11,8 +11,8 @@ import telegram import sys import coloredlogs import requests -from dirty import Dirty -from sentry_sdk import configure_scope +import strings +from utils import Dirty, DirtyDelta, reply_msg logging.getLogger().disabled = True logger = logging.getLogger(__name__) @@ -30,22 +30,17 @@ sentry = raven.Client(config["Sentry"]["token"], hook_libraries=[]) telegram_bot = telegram.Bot(config["Telegram"]["bot_token"]) +main_chat_id = config["Telegram"]["main_group"] -def update_block(session: db.Session, block: list, delay: float=0, change_callback: typing.Callable=None): +def update_block(session: db.Session, block: list, delay: float = 0, change_callback: typing.Callable = None): for item in block: logger.debug(f"Updating {repr(item)}.") t = time.clock() try: change = item.update(session=session) except requests.exceptions.HTTPError as e: - with configure_scope() as scope: - if str(e.response.status_code).startswith("5"): - scope.level = "warning" - logger.warning(f"Server error {sys.exc_info()} while updating {repr(item)}.") - else: - scope.level = "error" - logger.error(f"Error {sys.exc_info()} while updating {repr(item)}.") + logger.error(f"Error {sys.exc_info()} while updating {repr(item)}.") sentry.extra_context({ "item": repr(item), "response": { @@ -76,12 +71,11 @@ def new_dota_rank(item: db.Dota, change): f" {item.get_rank_name()} {item.get_rank_number()} su Dota 2!") except Exception: logger.warning(f"Couldn't notify on Telegram: {item}") + sentry.captureException() -def new_lol_rank(item, change: typing.Tuple[Dirty]): +def new_lol_rank(item, change: typing.Tuple[Dirty, Dirty, Dirty]): # It always gets called, even when there is no change - # Assignment inspection is wrong - # noinspection PyTupleAssignmentBalance solo, flex, twtr = change try: if solo: @@ -107,11 +101,65 @@ def new_lol_rank(item, change: typing.Tuple[Dirty]): parse_mode="Markdown") except Exception: logger.warning(f"Couldn't notify on Telegram: {item}") + sentry.captureException() + + +def osu_pp_change(item, change: typing.Tuple[DirtyDelta, DirtyDelta, DirtyDelta, DirtyDelta]): + std, taiko, catch, mania = change + try: + if std.delta >= 1: + telegram_bot.send_message(config["Telegram"]["main_group"], + f"✳️ {item.royal.username} ha ora {int(std.value)}pp (+{int(std.delta)}) su osu!", + parse_mode="HTML") + if taiko.delta >= 1: + telegram_bot.send_message(config["Telegram"]["main_group"], + f"✳️ {item.royal.username} ha ora {int(taiko.value)}pp (+{int(taiko.delta)}) su osu!taiko!", + parse_mode="HTML") + if catch.delta >= 1: + telegram_bot.send_message(config["Telegram"]["main_group"], + f"✳️ {item.royal.username} ha ora {int(catch.value)}pp (+{int(catch.delta)}) su osu!catch!", + parse_mode="HTML") + if mania.delta >= 1: + telegram_bot.send_message(config["Telegram"]["main_group"], + f"✳️ {item.royal.username} ha ora {int(mania.value)}pp (+{int(mania.delta)}) su osu!mania!", + parse_mode="HTML") + except Exception: + logger.warning(f"Couldn't notify on Telegram: {item}") + sentry.captureException() + + +def brawlhalla_rank_change(item, change: typing.Tuple[DirtyDelta, Dirty]): + solo, team = change + try: + if solo.delta >= 10: + reply_msg(telegram_bot, main_chat_id, strings.STATSUPDATE.BRAWLHALLA.SOLO, + username=item.steam.royal.telegram.mention(), + rating=solo.value, + delta=solo.difference_string()) + if team.is_dirty(): + partner = item.best_team_partner + if partner is None: + other = "qualcun altro" + else: + other = partner.steam.royal.telegram.mention() + reply_msg(telegram_bot, main_chat_id, strings.STATSUPDATE.BRAWLHALLA.TEAM, + username=item.steam.royal.telegram.mention(), + rating=team.value[1], + other=other) + except Exception: + logger.warning(f"Couldn't notify on Telegram: {item}") + sentry.captureException() def process(): while True: session = db.Session() + logger.info("Now updating League of Legends data.") + update_block(session, session.query(db.Brawlhalla).all(), delay=5, change_callback=brawlhalla_rank_change) + session.commit() + logger.info("Now updating osu! data.") + update_block(session, session.query(db.Osu).all(), delay=5, change_callback=osu_pp_change) + session.commit() logger.info("Now updating Steam data.") update_block(session, session.query(db.Steam).all()) session.commit() @@ -121,12 +169,6 @@ def process(): logger.info("Now updating League of Legends data.") update_block(session, session.query(db.LeagueOfLegends).all(), delay=5, change_callback=new_lol_rank) session.commit() - logger.info("Now updating osu! data.") - update_block(session, session.query(db.Osu).all(), delay=5) - session.commit() - logger.info("Now updating Overwatch data.") - update_block(session, session.query(db.Overwatch).all(), delay=5) - session.commit() logger.info("Pausing for 30 minutes.") time.sleep(1800) diff --git a/strings.py b/strings.py new file mode 100644 index 00000000..ea16633b --- /dev/null +++ b/strings.py @@ -0,0 +1,268 @@ +import utils +import dice +import typing + + +class SafeDict(dict): + def __missing__(self, key): + return "{" + key + "}" + + +def safely_format_string(string: str, words: typing.Dict[str, str] = None, ignore_escaping=False) -> str: + if words is None: + words = {} + if ignore_escaping: + escaped = words + else: + escaped = {} + for key in words: + escaped[key] = str(words[key]).replace("<", "<").replace(">", ">") + return string.format_map(SafeDict(**escaped)) + + +# Generic telegram errors +class TELEGRAM: + BOT_STARTED = "✅ Hai autorizzato il bot ad inviarti messaggi privati." + FIORYGI_AWARDED = "⭐️ {mention} è stato premiato con {number} {fiorygi} per {reason}!" + + class ERRORS: + CRITICAL_ERROR = "☢ ERRORE CRITICO!\nIl bot ha ignorato il comando.\nUna segnalazione di errore è stata automaticamente mandata a @Steffo.\n\nDettagli dell'errore:\n
{exc_info}
" + CRITICAL_ERROR_QUERY = "☢ ERRORE CRITICO!" + UNAUTHORIZED_USER = "⚠ Non sono autorizzato a inviare messaggi a {mention}.\nPer piacere, {mention}, inviami un messaggio in privata!" + UNAUTHORIZED_GROUP = "⚠ Non sono autorizzato a inviare messaggi in {group}.\n@Steffo, aggiungimi al gruppo o concedimi i permessi!" + + +PONG = "🏓 Pong!" +ESCAPE = "{text}" + + +# Ah, non lo so io. +class AHNONLOSOIO: + ONCE = "😐 Ah, non lo so io!" + AGAIN = "😐 Ah, non lo so nemmeno io..." + + +# Bridge commands between Discord and Telegram +class BRIDGE: + SUCCESS = "✅ Comando inoltrato a Discord." + FAILURE = "❎ Errore nell'esecuzione del comando su Discord." + + class ERRORS: + INVALID_SYNTAX = "⚠ Non hai specificato un comando!\nSintassi: /bridge (comando)" + INACTIVE_BRIDGE = "⚠ Il collegamento tra Telegram e Discord non è attivo al momento." + + +# Random spellslinging +class CAST: + class ERRORS: + NOT_YET_AVAILABLE = "⚠ Il nuovo cast non è ancora disponibile! Per un'anteprima sulle nuove funzioni, usa /spell." + + +# Ciao Ruozi! +class CIAORUOZI: + THE_LEGEND_HIMSELF = "👋 Ciao me!" + SOMEBODY_ELSE = "👋 Ciao Ruozi!" + + +# The /color meme, from Octeon +COLOR = "I am sorry, unknown error occured during working with your request, Admin were notified" + + +# Diario +class DIARIO: + SUCCESS = "✅ Riga aggiunta al diario:\n{diario}" + ANONYMOUS = "Anonimo" + + class ERRORS: + INVALID_SYNTAX = "⚠ Sintassi del comando errata.\nSintassi: /diario (frase), oppure rispondi a un messaggio con /diario." + NO_TEXT = "⚠ Il messaggio a cui hai risposto non contiene testo." + + +# Diario search +class DIARIOSEARCH: + HEADER = "ℹ️ Risultati della ricerca di {term}:\n" + + class ERRORS: + INVALID_SYNTAX = "⚠ Non hai specificato un termine da cercare!\nSintassi: /{command} (termine)" + RESULTS_TOO_LONG = "⚠ Sono presenti troppi risultati da visualizzare! Prova a restringere la ricerca." + + +# Eat! +class EAT: + FOODS = { + "_default": "🍗 Hai mangiato {food}!\nMa non succede nulla.", + "tonnuooooooro": "👻 Il {food} che hai mangiato era posseduto.\nSpooky!", + "uranio": "☢️ L'{food} che hai mangiato era radioattivo.\nStai brillando di verde!", + "pollo": '🍗 Il {food} che hai appena mangiato proveniva dallo spazio.\nCoccodè?', + "ragno": "🕸 Hai mangiato un {food}.\nEwww!", + "curry": "🔥 BRUCIAAAAAAAAAA! Il {food} era piccantissimo!\nStai sputando fiamme!", + "torta": "⬜️ Non hai mangiato niente.\nLa {food} è una menzogna!", + "cake": "⬜️ Non hai mangiato niente.\nThe {food} is a lie!", + "biscotto": "🍪 Hai mangiato un {food} di contrabbando.\nL'Inquisizione non lo saprà mai!", + "biscotti": "🍪 Hai mangiato tanti {food} di contrabbando.\nAttento! L'Inquisizione è sulle tue tracce!", + "tango": "🌳 Hai mangiato un {food}, e un albero insieme ad esso.\nSenti il tuo corpo curare le tue ferite.", + "sasso": "🥌 Il {food} che hai mangiato era duro come un {food}\nStai soffrendo di indigestione!", + "gnocchetti": "🥘 Ullà, sono duri 'sti {food}!\nFai fatica a digerirli.", + "tide pods": "☣️ I {food} che hai mangiato erano buonissimi.\nStai sbiancando!" + } + + class ERRORS: + INVALID_SYNTAX = "⚠ Non hai specificato cosa mangiare!\nSintassi: /eat (cibo)" + + +# Emojify a string +class EMOJIFY: + RESPONSE = "{emojified}" + + class ERRORS: + INVALID_SYNTAX = "⚠ Non hai specificato una frase!\nSintassi: /emojify (testo)" + + +# Royalnet linking +class LINK: + SUCCESS = "✅ Collegamento riuscito!" + + class ERRORS: + INVALID_SYNTAX = "⚠ Non hai specificato un username!\nSintassi: /link (username)" + NOT_FOUND = "⚠ Non esiste nessun account Royalnet con quel nome.\nNota: gli username sono case-sensitive, e iniziano sempre con una maiuscola!" + ALREADY_EXISTING = "⚠ Questo account è già collegato a un account Royalnet." + ROYALNET_NOT_LINKED = "⚠ Il tuo account Telegram non è connesso a Royalnet! Connettilo con /link (username)." + + +# Markov strings +class MARKOV: + class ERRORS: + NO_MODEL = "⚠ La catena di Markov non è disponibile." + GENERATION_FAILED = "⚠ markovify non è riuscito a generare una frase. Prova di nuovo?\n E' un'avvenimento sorprendentemente raro..." + SPECIFIC_WORD_FAILED = "⚠ markovify non è riuscito a generare una frase partendo da questa parola. Provane una diversa..." + MISSING_WORD = "⚠ La parola specificata non è presente nella catena di Markov. Provane una diversa..." + + +# Matchmaking service strings +class MATCHMAKING: + EMOJIS = { + "ready": "🔵", + "wait_for_me": "🕒", + "maybe": "❓", + "ignore": "❌", + "close": "🚩", + "cancel": "🗑" + } + + ENUM_TO_EMOJIS = { + utils.MatchmakingStatus.READY: EMOJIS["ready"], + utils.MatchmakingStatus.WAIT_FOR_ME: EMOJIS["wait_for_me"], + utils.MatchmakingStatus.MAYBE: EMOJIS["maybe"], + utils.MatchmakingStatus.IGNORED: EMOJIS["ignore"], + } + + BUTTONS = { + "match_ready": f"{EMOJIS['ready']} Sono pronto per iniziare!", + "match_wait_for_me": f"{EMOJIS['wait_for_me']} Ci sarò, aspettatemi!", + "match_maybe": f"{EMOJIS['maybe']} Forse vengo, se non ci sono fate senza di me.", + "match_ignore": f"{EMOJIS['ignore']} Non ci sarò.", + "match_close": f"{EMOJIS['close']} ADMIN: Avvia la partita", + "match_cancel": f"{EMOJIS['cancel']} ADMIN: Annulla la partita" + } + + TICKER_TEXT = { + "match_ready": f"{EMOJIS['ready']} Hai detto che sei pronto per giocare!", + "match_wait_for_me": f"{EMOJIS['wait_for_me']} Hai chiesto agli altri di aspettarti.", + "match_maybe": f"{EMOJIS['maybe']} Hai detto che forse ci sarai.", + "match_ignore": f"{EMOJIS['ignore']} Non hai intenzione di partecipare.", + "match_close": f"{EMOJIS['close']} Hai notificato tutti che la partita sta iniziando.", + "match_cancel": f"{EMOJIS['cancel']} Hai annullato la partita." + } + + GAME_START = { + int(utils.MatchmakingStatus.READY): "🔵 Che {match_title} abbia inizio!", + int(utils.MatchmakingStatus.WAIT_FOR_ME): "🕒 Sbrigati! {match_title} sta per iniziare!", + int(utils.MatchmakingStatus.MAYBE): "❓ {match_title} sta iniziando. Se vuoi partecipare, fai in fretta!", + } + + class ERRORS: + INVALID_SYNTAX = "⚠ Sintassi del comando errata.\nSintassi:
/mm [minplayers-][maxplayers] ['per'] (gamename) \\n[descrizione]
" + NOT_ADMIN = "⚠ Non sei il creatore di questo match!" + MATCH_CLOSED = "⚠ Il matchmaking per questa partita è terminato." + + +# Pug sender +class PUG: + HERE_HAVE_A_PUG = '🐶 Ecco, tieni un carlino.' + + class ERRORS: + PRIVATE_CHAT_ONLY = "⚠ Foto di carlini possono essere inviate esclusivamente in chat privata, in seguito al Disegno di Legge Intergalattico n. 5116." + + +# Dice roller +class ROLL: + SUCCESS = "🎲 {result}" + + SYMBOLS = { + dice.elements.Div: "/", + dice.elements.Mul: "*", + dice.elements.Sub: "-", + dice.elements.Add: "+", + dice.elements.Modulo: "%", + dice.elements.AddEvenSubOdd: "+-", + dice.elements.Highest: "h", + dice.elements.Lowest: "l", + dice.elements.Middle: "m", + dice.elements.Again: "a", + dice.elements.Successes: "e", + dice.elements.SuccessFail: "f", + dice.elements.ArrayAdd: ".+", + dice.elements.ArraySub: ".-", + dice.elements.Array: ",", + dice.elements.Extend: "|", + dice.elements.Reroll: "r", + dice.elements.Explode: "x", + dice.elements.ForceReroll: "rr" + } + + class ERRORS: + INVALID_SYNTAX = "⚠ Sintassi del tiro di dadi non valida." + DICE_ERROR = "⚠ Il tiro di dadi è fallito." + + +# Ship creator +class SHIP: + RESULT = "💕 {one} + {two} = {result}" + + class ERRORS: + INVALID_SYNTAX = "⚠ Non hai specificato correttamente i due nomi!\nSintassi: /ship (nome) (nome)" + INVALID_NAMES = "⚠ I nomi specificati non sono validi.\nRiprova con dei nomi diversi!" + + +# Get information about a spell +class SPELL: + HEADER = "🔍 La magia {name} ha le seguenti proprietà (v{version}):\n" + ACCURACY = "Precisione - {accuracy}%\n" + DAMAGE = "Danni - {number}d{type}{constant} (in media {avg})\n" + TYPE = "Tipo - {type}\n" + REPEAT = "Multiattacco - ×{repeat}\n" + HEALING = "Cura - {number}d{type}{constant} (in media {avg})\n" + STAT = "Attrib. - {name}{change}\n" + STATUS_EFFECT = "Infligge - {effect} ({chance}% di probabilità)" + NOTHING = "Chi la usa sguazza nell'acqua, senza ottenere alcun effetto." + + class ERRORS: + INVALID_SYNTAX = "⚠ Non hai specificato la magia di cui vuoi conoscere i dettagli!\nSintassi: /spell (nome)" + + +# Game stats updates +class STATSUPDATE: + class BRAWLHALLA: + SOLO = "✳️ {username} ha ora {rating} ({delta}) Elo 1v1 su Brawlhalla!" + TEAM = "✳️ {username}+{other} hanno ora {rating} Elo 2v2 su Brawlhalla!" + + +# Secondo me, è colpa delle stringhe. +SMECDS = "🤔 Secondo me, è colpa {ds}." + + +# Wiki notifications +class WIKI: + PAGE_LOCKED = '🔒 La pagina wiki {key} è stata bloccata da {user}.' + PAGE_UNLOCKED = '🔓 La pagina wiki {key} è stata sbloccata da {user}.' + PAGE_UPDATED = '📖 La pagina wiki {key} è stata modificata da {user}.\n{reason} [{change}]' diff --git a/telegrambot.py b/telegrambot.py index 37d67d3d..131c045f 100644 --- a/telegrambot.py +++ b/telegrambot.py @@ -2,30 +2,40 @@ import datetime import random import typing import db -import errors -import stagismo +from utils import smecds, cast, errors, emojify, reply_msg # python-telegram-bot has a different name # noinspection PyPackageRequirements -from telegram import Bot, Update, InlineKeyboardMarkup, InlineKeyboardButton +import telegram # noinspection PyPackageRequirements from telegram.ext import Updater, CommandHandler, CallbackQueryHandler +# noinspection PyPackageRequirements +from telegram.error import TimedOut, Unauthorized, BadRequest, TelegramError import dice import sys import os -import cast import re import logging import configparser import markovify import raven import coloredlogs +import strings +import time +import requests +IKMarkup = telegram.InlineKeyboardMarkup +IKButton = telegram.InlineKeyboardButton -# Markov model +# Markov models try: - with open("markovmodel.json") as file: - model = markovify.Text.from_json(file.read()) + with open("markovmodels/default.json") as file: + default_model = markovify.Text.from_json(file.read()) except Exception: - model = None + default_model = None +try: + with open("markovmodels/dnd4.json") as file: + dnd4_model = markovify.Text.from_json(file.read()) +except Exception: + dnd4_model = None logging.getLogger().disabled = True logger = logging.getLogger(__name__) @@ -35,6 +45,7 @@ coloredlogs.install(level="DEBUG", logger=logger) # Init the config reader config = configparser.ConfigParser() config.read("config.ini") +main_group_id = int(config["Telegram"]["main_group"]) discord_connection = None @@ -45,75 +56,106 @@ sentry = raven.Client(config["Sentry"]["token"], hook_libraries=[]) -def catch_and_report(func: "function"): - def new_func(bot: Bot, update: Update): +def reply(bot: telegram.Bot, update: telegram.Update, string: str, ignore_escaping=False, disable_web_page_preview=True, **kwargs) -> telegram.Message: + while True: + try: + return reply_msg(bot, update.message.chat.id, string, ignore_escaping=ignore_escaping, disable_web_page_preview=disable_web_page_preview, **kwargs) + except Unauthorized: + if update.message.chat.type == telegram.Chat.PRIVATE: + return reply_msg(bot, main_group_id, strings.TELEGRAM.ERRORS.UNAUTHORIZED_USER, + mention=update.message.from_user.mention_html()) + else: + return reply_msg(bot, main_group_id, strings.TELEGRAM.ERRORS.UNAUTHORIZED_GROUP, + group=(update.message.chat.title or update.message.chat.id)) + except TimedOut: + time.sleep(1) + pass + + +# noinspection PyUnresolvedReferences +def command(func: "function"): + def new_func(bot: telegram.Bot, update: telegram.Update): # noinspection PyBroadException try: + bot.send_chat_action(update.message.chat.id, telegram.ChatAction.TYPING) return func(bot, update) + except TimedOut: + logger.warning(f"Telegram timed out in {update}") except Exception: logger.error(f"Critical error: {sys.exc_info()}") # noinspection PyBroadException try: - bot.send_message(int(config["Telegram"]["main_group"]), - "☢ **ERRORE CRITICO!** \n" - f"Il bot ha ignorato il comando.\n" - f"Una segnalazione di errore è stata automaticamente mandata a @Steffo.\n\n" - f"Dettagli dell'errore:\n" - f"```\n" - f"{sys.exc_info()}\n" - f"```", parse_mode="Markdown") + reply_msg(bot, main_group_id, strings.TELEGRAM.ERRORS.CRITICAL_ERROR, + exc_info=repr(sys.exc_info())) except Exception: logger.error(f"Double critical error: {sys.exc_info()}") - if not __debug__: - sentry.user_context({ - "id": update.effective_user.id, - "telegram": { - "username": update.effective_user.username, - "first_name": update.effective_user.first_name, - "last_name": update.effective_user.last_name - } - }) - sentry.extra_context({ - "update": update.to_dict() - }) - sentry.captureException() + sentry.user_context({ + "id": update.effective_user.id, + "telegram": { + "username": update.effective_user.username, + "first_name": update.effective_user.first_name, + "last_name": update.effective_user.last_name + } + }) + sentry.extra_context({ + "update": update.to_dict() + }) + sentry.captureException() return new_func -@catch_and_report -def cmd_ping(bot: Bot, update: Update): - bot.send_message(update.message.chat.id, "🏓 Pong!") +# noinspection PyUnresolvedReferences +def database_access(func: "function"): + def new_func(bot: telegram.Bot, update: telegram.Update): + try: + session = db.Session() + return func(bot, update, session) + except Exception: + logger.error(f"Database error: {sys.exc_info()}") + sentry.captureException() + finally: + try: + session.close() + except Exception: + pass + return new_func -@catch_and_report -def cmd_register(bot: Bot, update: Update): - session = db.Session() +@command +def cmd_ping(bot: telegram.Bot, update: telegram.Update): + reply(bot, update, strings.PONG) + + +@command +@database_access +def cmd_link(bot: telegram.Bot, update: telegram.Update, session: db.Session): try: username = update.message.text.split(" ", 1)[1] except IndexError: - bot.send_message(update.message.chat.id, "⚠️ Non hai specificato un username!") + reply(bot, update, strings.LINK.ERRORS.INVALID_SYNTAX) session.close() return try: t = db.Telegram.create(session, royal_username=username, telegram_user=update.message.from_user) + except errors.NotFoundError: + reply(bot, update, strings.LINK.ERRORS.NOT_FOUND) + session.close() + return except errors.AlreadyExistingError: - bot.send_message(update.message.chat.id, "⚠ Il tuo account Telegram è già collegato a un account RYG o" - " l'account RYG che hai specificato è già collegato a un account" - " Telegram.") + reply(bot, update, strings.LINK.ERRORS.ALREADY_EXISTING) session.close() return session.add(t) session.commit() - bot.send_message(update.message.chat.id, "✅ Sincronizzazione completata!") - session.close() + reply(bot, update, strings.LINK.SUCCESS) -@catch_and_report -def cmd_discord(bot: Bot, update: Update): +@command +def cmd_cv(bot: telegram.Bot, update: telegram.Update): if discord_connection is None: - bot.send_message(update.message.chat.id, "⚠ Il bot non è collegato a Discord al momento.") + reply(bot, update, strings.BRIDGE.ERRORS.INACTIVE_BRIDGE) return # dirty hack as usual if update.message.text.endswith("full"): @@ -121,194 +163,249 @@ def cmd_discord(bot: Bot, update: Update): else: discord_connection.send("get cv") message = discord_connection.recv() - bot.send_message(update.message.chat.id, message, disable_web_page_preview=True, parse_mode="HTML") + reply(bot, update, message) -@catch_and_report -def cmd_cast(bot: Bot, update: Update): - try: - spell: str = update.message.text.split(" ", 1)[1] - except IndexError: - bot.send_message(update.message.chat.id, "⚠️ Non hai specificato nessun incantesimo!\n" - "Sintassi corretta: `/cast `", parse_mode="Markdown") - return - # Open a new db session - session = db.Session() - # Find a target for the spell - target = random.sample(session.query(db.Telegram).all(), 1)[0] - # Close the session - session.close() - # END - bot.send_message(update.message.chat.id, cast.cast(spell_name=spell, - target_name=target.username if target.username is not None - else target.first_name, platform="telegram"), - parse_mode="HTML") +@command +def cmd_cast(bot: telegram.Bot, update: telegram.Update): + reply(bot, update, strings.CAST.ERRORS.NOT_YET_AVAILABLE) -@catch_and_report -def cmd_color(bot: Bot, update: Update): - bot.send_message(update.message.chat.id, "I am sorry, unknown error occured during working with your request," - " Admin were notified") +@command +def cmd_color(bot: telegram.Bot, update: telegram.Update): + reply(bot, update, strings.COLOR) -@catch_and_report -def cmd_smecds(bot: Bot, update: Update): - ds = random.sample(stagismo.listona, 1)[0] - bot.send_message(update.message.chat.id, f"Secondo me, è colpa {ds}.") +@command +def cmd_smecds(bot: telegram.Bot, update: telegram.Update): + ds = random.sample(smecds, 1)[0] + reply(bot, update, strings.SMECDS, ds=ds) -@catch_and_report -def cmd_ciaoruozi(bot: Bot, update: Update): +@command +def cmd_ciaoruozi(bot: telegram.Bot, update: telegram.Update): if update.message.from_user.username.lstrip("@") == "MeStakes": - bot.send_message(update.message.chat.id, "Ciao me!") + reply(bot, update, strings.CIAORUOZI.THE_LEGEND_HIMSELF) else: - bot.send_message(update.message.chat.id, "Ciao Ruozi!") + reply(bot, update, strings.CIAORUOZI.SOMEBODY_ELSE) -@catch_and_report -def cmd_ahnonlosoio(bot: Bot, update: Update): +@command +def cmd_ahnonlosoio(bot: telegram.Bot, update: telegram.Update): if update.message.reply_to_message is not None and update.message.reply_to_message.text in [ - "/ahnonlosoio", "/ahnonlosoio@royalgamesbot", "Ah, non lo so io!", "Ah, non lo so neppure io!" + "/ahnonlosoio", "/ahnonlosoio@royalgamesbot", strings.AHNONLOSOIO.ONCE, strings.AHNONLOSOIO.AGAIN ]: - bot.send_message(update.message.chat.id, "Ah, non lo so neppure io!") + reply(bot, update, strings.AHNONLOSOIO.AGAIN) else: - bot.send_message(update.message.chat.id, "Ah, non lo so io!") + reply(bot, update, strings.AHNONLOSOIO.ONCE) -@catch_and_report -def cmd_balurage(bot: Bot, update: Update): - session = db.Session() +@command +@database_access +def cmd_balurage(bot: telegram.Bot, update: telegram.Update, session: db.Session): + user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() + if user is None: + reply(bot, update, strings.LINK.ERRORS.ROYALNET_NOT_LINKED) + return try: - user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() - if user is None: - bot.send_message(update.message.chat.id, - "⚠ Il tuo account Telegram non è registrato al RYGdb!\n\n" - "Registrati con `/register@royalgamesbot `.", - parse_mode="Markdown") - return - try: - reason = update.message.text.split(" ", 1)[1] - except IndexError: - reason = None - br = db.BaluRage(royal_id=user.royal_id, reason=reason) - session.add(br) - session.commit() - bot.send_message(update.message.chat.id, f"😡 Stai sfogando la tua ira sul bot!") - except Exception: - raise - finally: - session.close() + reason = update.message.text.split(" ", 1)[1] + except IndexError: + reason = None + br = db.BaluRage(royal_id=user.royal_id, reason=reason) + session.add(br) + session.commit() + bot.send_message(update.message.chat.id, f"😡 Stai sfogando la tua ira sul bot!") -@catch_and_report -def cmd_diario(bot: Bot, update: Update): - session = db.Session() +def parse_diario(session: db.Session, text: str): + match = re.match(r'"?([^"]+)"? (?:—|-{1,2}) ?@?([A-Za-z0-9_]+)$', text) + if match is None: + return None, text + text_string = match.group(1) + author_string = match.group(2).lower() + royal = session.query(db.Royal).filter(db.func.lower(db.Royal.username) == author_string).first() + if royal is not None: + author = royal.telegram[0] + return author, text_string + author = session.query(db.Telegram).filter(db.func.lower(db.Telegram.username) == author_string).first() + if author is not None: + return author, text_string + return None, text_string + + +@command +@database_access +def cmd_diario(bot: telegram.Bot, update: telegram.Update, session: db.Session): + user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() + if user is None: + reply(bot, update, strings.LINK.ERRORS.ROYALNET_NOT_LINKED) + return try: - user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() - if user is None: - bot.send_message(update.message.chat.id, "⚠ Il tuo account Telegram non è registrato al RYGdb!" - " Registrati con `/register@royalgamesbot `.", - parse_mode="Markdown") + text = update.message.text.split(" ", 1)[1] + saver = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() + author, actual_text = parse_diario(session, text) + except IndexError: + if update.message.reply_to_message is None: + reply(bot, update, strings.DIARIO.ERRORS.INVALID_SYNTAX) return - try: - text = update.message.text.split(" ", 1)[1] - author = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() - saver = author - except IndexError: - if update.message.reply_to_message is None: - bot.send_message(update.message.chat.id, - f"⚠ Non hai specificato cosa aggiungere al diario!\n\n" - f"Puoi rispondere `/diario@royalgamesbot` al messaggio che vuoi salvare nel diario" - f" oppure scrivere `/diario@royalgamesbot `" - f" per aggiungere quel messaggio nel diario.", - parse_mode="Markdown") - return - text = update.message.reply_to_message.text + text = update.message.reply_to_message.text + if update.message.forward_from: + author = session.query(db.Telegram) \ + .filter_by(telegram_id=update.message.forward_from.id) \ + .one_or_none() + else: author = session.query(db.Telegram)\ .filter_by(telegram_id=update.message.reply_to_message.from_user.id)\ .one_or_none() - saver = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() - if text is None: - bot.send_message(update.message.chat.id, f"⚠ Il messaggio a cui hai risposto non contiene testo.") - return - diario = db.Diario(timestamp=datetime.datetime.now(), - saver=saver, - author=author, - text=text) - session.add(diario) - session.commit() + saver = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() + if text is None: + reply(bot, update, strings.DIARIO.ERRORS.NO_TEXT) + return + diario = db.Diario(timestamp=datetime.datetime.now(), + saver=saver, + author=author, + text=actual_text) + session.add(diario) + session.commit() + reply(bot, update, strings.DIARIO.SUCCESS, ignore_escaping=True, diario=diario.to_telegram()) + + +@command +@database_access +def cmd_vote(bot: telegram.Bot, update: telegram.Update, session: db.Session): + user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() + if user is None: bot.send_message(update.message.chat.id, - f"✅ Riga [#{diario.id}](https://ryg.steffo.eu/diario#entry-{diario.id}) aggiunta al diario!", - parse_mode="Markdown", disable_web_page_preview=True) - except Exception: - raise - finally: - session.close() - - -@catch_and_report -def cmd_vote(bot: Bot, update: Update): - session = db.Session() + "⚠ Il tuo account Telegram non è registrato al RYGdb!" + " Registrati con `/register@royalgamesbot `.", parse_mode="Markdown") + return try: - user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() - if user is None: - bot.send_message(update.message.chat.id, - "⚠ Il tuo account Telegram non è registrato al RYGdb!" - " Registrati con `/register@royalgamesbot `.", parse_mode="Markdown") - return - try: - _, mode, question = update.message.text.split(" ", 2) - except IndexError: - bot.send_message(update.message.chat.id, - "⚠ Non hai specificato tutti i parametri necessari!" - "Sintassi: `/vote@royalgamesbot `", parse_mode="Markdown") - return - if mode == "public": - vote = db.VoteQuestion(question=question, anonymous=False) - elif mode == "secret": - vote = db.VoteQuestion(question=question, anonymous=True) - else: - bot.send_message(update.message.chat.id, - "⚠ Non hai specificato una modalità valida!" - "Sintassi: `/vote@royalgamesbot `", parse_mode="Markdown") - return - session.add(vote) - session.flush() - inline_keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("🔵 Sì", callback_data="vote_yes")], - [InlineKeyboardButton("🔴 No", callback_data="vote_no")], - [InlineKeyboardButton("⚫️ Astieniti", callback_data="vote_abstain")]]) - message = bot.send_message(update.message.chat.id, vote.generate_text(session=session), - reply_markup=inline_keyboard, - parse_mode="HTML") - vote.message_id = message.message_id - session.commit() - except Exception: - raise - finally: - session.close() - - -@catch_and_report -def on_callback_query(bot: Bot, update: Update): - if update.callback_query.data == "vote_yes": - choice = db.VoteChoices.YES - emoji = "🔵" - elif update.callback_query.data == "vote_no": - choice = db.VoteChoices.NO - emoji = "🔴" - elif update.callback_query.data == "vote_abstain": - choice = db.VoteChoices.ABSTAIN - emoji = "⚫️" + _, mode, question = update.message.text.split(" ", 2) + except IndexError: + bot.send_message(update.message.chat.id, + "⚠ Non hai specificato tutti i parametri necessari!" + "Sintassi: `/vote@royalgamesbot `", parse_mode="Markdown") + return + if mode == "public": + vote = db.VoteQuestion(question=question, anonymous=False) + elif mode == "secret": + vote = db.VoteQuestion(question=question, anonymous=True) else: - raise NotImplementedError() - if update.callback_query.data.startswith("vote_"): + bot.send_message(update.message.chat.id, + "⚠ Non hai specificato una modalità valida!" + "Sintassi: `/vote@royalgamesbot `", parse_mode="Markdown") + return + session.add(vote) + session.flush() + inline_keyboard = IKMarkup([[IKButton("🔵 Sì", callback_data="vote_yes")], + [IKButton("🔴 No", callback_data="vote_no")], + [IKButton("⚫️ Astieniti", callback_data="vote_abstain")]]) + message = bot.send_message(update.message.chat.id, vote.generate_text(session=session), + reply_markup=inline_keyboard, + parse_mode="HTML") + vote.message_id = message.message_id + session.commit() + + +def generate_search_message(term, entries): + msg = strings.DIARIOSEARCH.HEADER.format(term=term) + if len(entries) < 100: + for entry in entries[:5]: + msg += f'#{entry.id} di {entry.author or "Anonimo"}\n{entry.text}\n\n' + if len(entries) > 5: + msg += "I termini comapiono anche nelle righe:\n" + for entry in entries[5:]: + msg += f'#{entry.id} ' + else: + for entry in entries[:100]: + msg += f'#{entry.id} ' + for entry in entries[100:]: + msg += f"#{entry.id} " + return msg + + +@command +@database_access +def cmd_search(bot: telegram.Bot, update: telegram.Update, session: db.Session): + try: + query = update.message.text.split(" ", 1)[1] + except IndexError: + reply(bot, update, strings.DIARIOSEARCH.ERRORS.INVALID_SYNTAX, command="search") + return + query = query.replace('%', '\\%').replace('_', '\\_') + entries = session.query(db.Diario)\ + .filter(db.Diario.text.op("~*")(r"(?:[\s\.,:;!?\"'<{([]+|^)" + + query + + r"(?:[\s\.,:;!?\"'>\})\]]+|$)"))\ + .order_by(db.Diario.id.desc())\ + .all() + bot.send_message(update.message.chat.id, generate_search_message(f"{query}", entries), parse_mode="HTML") + + +@command +@database_access +def cmd_regex(bot: telegram.Bot, update: telegram.Update, session: db.Session): + try: + query = update.message.text.split(" ", 1)[1] + except IndexError: + reply(bot, update, strings.DIARIOSEARCH.ERRORS.INVALID_SYNTAX, command="regex") + return + query = query.replace('%', '\\%').replace('_', '\\_') + entries = session.query(db.Diario).filter(db.Diario.text.op("~*")(query)).order_by(db.Diario.id.desc()).all() + try: + bot.send_message(update.message.chat.id, generate_search_message(f"{query}", entries), parse_mode="HTML") + except (BadRequest, TelegramError): + reply(bot, update, strings.DIARIOSEARCH.ERRORS.RESULTS_TOO_LONG) + + +@command +@database_access +def cmd_mm(bot: telegram.Bot, update: telegram.Update, session: db.Session): + user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() + if user is None: + reply(bot, update, strings.LINK.ERRORS.ROYALNET_NOT_LINKED) + return + match = re.match(r"/(?:mm|matchmaking)(?:@royalgamesbot)?(?: (?:([0-9]+)-)?([0-9]+))? (?:per )?([A-Za-z0-9!\-_. ]+)(?:.*\n(.+))?", + update.message.text) + if match is None: + reply(bot, update, strings.MATCHMAKING.ERRORS.INVALID_SYNTAX) + return + min_players, max_players, match_name, match_desc = match.group(1, 2, 3, 4) + db_match = db.Match(timestamp=datetime.datetime.now(), + match_title=match_name, + match_desc=match_desc, + min_players=min_players, + max_players=max_players, + creator=user) + session.add(db_match) + session.flush() + inline_keyboard = IKMarkup([([IKButton(strings.MATCHMAKING.BUTTONS[key], callback_data=key)]) for key in strings.MATCHMAKING.BUTTONS]) + message = bot.send_message(config["Telegram"]["announcement_group"], db_match.generate_text(session=session), + parse_mode="HTML", + reply_markup=inline_keyboard) + db_match.message_id = message.message_id + session.commit() + + +def on_callback_query(bot: telegram.Bot, update: telegram.Update): + try: session = db.Session() - try: + if update.callback_query.data.startswith("vote_"): + if update.callback_query.data == "vote_yes": + status = db.VoteChoices.YES + emoji = "🔵" + elif update.callback_query.data == "vote_no": + status = db.VoteChoices.NO + emoji = "🔴" + elif update.callback_query.data == "vote_abstain": + status = db.VoteChoices.ABSTAIN + emoji = "⚫️" + else: + raise NotImplementedError() user = session.query(db.Telegram).filter_by(telegram_id=update.callback_query.from_user.id).one_or_none() if user is None: bot.answer_callback_query(update.callback_query.id, show_alert=True, - text="⚠ Il tuo account Telegram non è registrato al RYGdb!" - " Registrati con `/register@royalgamesbot `.", + text=strings.LINK.ERRORS.ROYALNET_NOT_LINKED, parse_mode="Markdown") return question = session.query(db.VoteQuestion)\ @@ -316,106 +413,177 @@ def on_callback_query(bot: Bot, update: Update): .one() answer = session.query(db.VoteAnswer).filter_by(question=question, user=user).one_or_none() if answer is None: - answer = db.VoteAnswer(question=question, choice=choice, user=user) + answer = db.VoteAnswer(question=question, choice=status, user=user) session.add(answer) bot.answer_callback_query(update.callback_query.id, text=f"Hai votato {emoji}.", cache_time=1) - elif answer.choice == choice: + elif answer.choice == status: session.delete(answer) bot.answer_callback_query(update.callback_query.id, text=f"Hai ritratto il tuo voto.", cache_time=1) else: - answer.choice = choice + answer.choice = status bot.answer_callback_query(update.callback_query.id, text=f"Hai cambiato il tuo voto in {emoji}.", cache_time=1) session.commit() - inline_keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("🔵 Sì", - callback_data="vote_yes")], - [InlineKeyboardButton("🔴 No", - callback_data="vote_no")], - [InlineKeyboardButton("⚫️ Astieniti", - callback_data="vote_abstain")]]) + inline_keyboard = IKMarkup([[IKButton("🔵 Sì", callback_data="vote_yes")], + [IKButton("🔴 No", callback_data="vote_no")], + [IKButton("⚫️ Astieniti", callback_data="vote_abstain")]]) bot.edit_message_text(message_id=update.callback_query.message.message_id, chat_id=update.callback_query.message.chat.id, text=question.generate_text(session), reply_markup=inline_keyboard, parse_mode="HTML") + elif update.callback_query.data.startswith("match_"): + user = session.query(db.Telegram).filter_by(telegram_id=update.callback_query.from_user.id).one_or_none() + if user is None: + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.LINK.ERRORS.ROYALNET_NOT_LINKED, + parse_mode="Markdown") + return + match = session.query(db.Match).filter_by(message_id=update.callback_query.message.message_id).one() + if update.callback_query.data == "match_close": + if not (match.creator == user or user.telegram_id == 25167391): + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.MATCHMAKING.ERRORS.NOT_ADMIN) + return + match.closed = True + for partecipation in match.players: + if int(partecipation.status) >= 1: + try: + reply_msg(bot, partecipation.user.telegram_id, strings.MATCHMAKING.GAME_START[int(partecipation.status)], **match.format_dict()) + except Unauthorized: + reply_msg(bot, main_group_id, strings.TELEGRAM.ERRORS.UNAUTHORIZED_USER, + mention=partecipation.user.mention()) + elif update.callback_query.data == "match_cancel": + if not (match.creator == user or user.telegram_id == 25167391): + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.MATCHMAKING.ERRORS.NOT_ADMIN) + return + match.closed = True + status = { + "match_ready": db.MatchmakingStatus.READY, + "match_wait_for_me": db.MatchmakingStatus.WAIT_FOR_ME, + "match_maybe": db.MatchmakingStatus.MAYBE, + "match_ignore": db.MatchmakingStatus.IGNORED, + "match_close": None, + "match_cancel": None, + }.get(update.callback_query.data) + if status: + if match.closed: + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.MATCHMAKING.ERRORS.MATCH_CLOSED) + return + partecipation = session.query(db.MatchPartecipation).filter_by(match=match, user=user).one_or_none() + if partecipation is None: + partecipation = db.MatchPartecipation(match=match, status=status.value, user=user) + session.add(partecipation) + else: + partecipation.status = status.value + session.commit() + bot.answer_callback_query(update.callback_query.id, + text=strings.MATCHMAKING.TICKER_TEXT[update.callback_query.data], + cache_time=1) + if not match.closed: + inline_keyboard = IKMarkup([([IKButton(strings.MATCHMAKING.BUTTONS[key], callback_data=key)]) for key in strings.MATCHMAKING.BUTTONS]) + else: + inline_keyboard = None + while True: + try: + bot.edit_message_text(message_id=update.callback_query.message.message_id, + chat_id=config["Telegram"]["announcement_group"], + text=match.generate_text(session), + reply_markup=inline_keyboard, + parse_mode="HTML") + break + except BadRequest: + break + except TimedOut: + time.sleep(1) + except Exception: + try: + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.TELEGRAM.ERRORS.CRITICAL_ERROR_QUERY) except Exception: - raise - finally: + pass + logger.error(f"Critical error: {sys.exc_info()}") + sentry.user_context({ + "id": update.effective_user.id, + "telegram": { + "username": update.effective_user.username, + "first_name": update.effective_user.first_name, + "last_name": update.effective_user.last_name + } + }) + sentry.extra_context({ + "update": update.to_dict() + }) + sentry.captureException() + finally: + try: + # noinspection PyUnboundLocalVariable session.close() + except Exception: + pass -@catch_and_report -def cmd_eat(bot: Bot, update: Update): +@command +def cmd_eat(bot: telegram.Bot, update: telegram.Update): try: food: str = update.message.text.split(" ", 1)[1].capitalize() except IndexError: - bot.send_message(update.message.chat.id, "⚠️ Non hai specificato cosa mangiare!\n" - "Sintassi corretta: `/eat `", parse_mode="Markdown") + reply(bot, update, strings.EAT.ERRORS.INVALID_SYNTAX) return - if "tonnuooooooro" in food.lower(): - bot.send_message(update.message.chat.id, "👻 Il pesce che hai mangiato era posseduto.\n" - "Spooky!") - return - bot.send_message(update.message.chat.id, f"🍗 Hai mangiato {food}!") + reply(bot, update, strings.EAT.FOODS.get(food.lower(), strings.EAT.FOODS["_default"]), food=food) -@catch_and_report -def cmd_ship(bot: Bot, update: Update): +@command +def cmd_ship(bot: telegram.Bot, update: telegram.Update): try: - _, name_one, name_two = update.message.text.split(" ", 2) + names = update.message.text.split(" ") + name_one = names[1] + name_two = names[2] except ValueError: - bot.send_message(update.message.chat.id, "⚠️ Non hai specificato correttamente i due nomi!\n" - "Sintassi corretta: `/ship `", parse_mode="Markdown") + reply(bot, update, strings.SHIP.ERRORS.INVALID_SYNTAX) return name_one = name_one.lower() name_two = name_two.lower() - part_one = re.search(r"^[A-Za-z][^aeiouAEIOU]*[aeiouAEIOU]?", name_one) - part_two = re.search(r"[^aeiouAEIOU]*[aeiouAEIOU]?[A-Za-z]$", name_two) - try: - mixed = part_one.group(0) + part_two.group(0) - except Exception: - bot.send_message(update.message.chat.id, "⚠ I nomi specificati non sono validi.\n" - "Riprova con dei nomi diversi!") + match_one = re.search(r"^[A-Za-z][^aeiouAEIOU]*[aeiouAEIOU]?", name_one) + if match_one is None: + part_one = name_one[:int(len(name_one) / 2)] + else: + part_one = match_one.group(0) + match_two = re.search(r"[^aeiouAEIOU]*[aeiouAEIOU]?[A-Za-z]$", name_two) + if match_two is None: + part_two = name_two[int(len(name_two) / 2):] + else: + part_two = match_two.group(0) + mixed = part_one + part_two # TODO: find out what exceptions this could possibly raise + reply(bot, update, strings.SHIP.RESULT, + one=name_one.capitalize(), + two=name_two.capitalize(), + result=mixed.capitalize()) + + +@command +def cmd_bridge(bot: telegram.Bot, update: telegram.Update): + if discord_connection is None: + reply(bot, update, strings.BRIDGE.ERRORS.INACTIVE_BRIDGE) return - if part_one is None or part_two is None: - bot.send_message(update.message.chat.id, "⚠ I nomi specificati non sono validi.\n" - "Riprova con dei nomi diversi!") - return - bot.send_message(update.message.chat.id, f"💕 {name_one.capitalize()} + {name_two.capitalize()} =" - f" {mixed.capitalize()}") - - -@catch_and_report -def cmd_profile(bot: Bot, update: Update): - session = db.Session() - user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).join(db.Royal).one_or_none() - session.close() - if user is None: - bot.send_message(update.message.chat.id, "⚠ Non sei connesso a Royalnet!\n" - "Per registrarti, utilizza il comando /register.") - return - bot.send_message(update.message.chat.id, f"👤 [Profilo di {user.royal.username}]" - f"(http://ryg.steffo.eu/profile/{user.royal.username})\n" - f"Attualmente, hai **{user.royal.fiorygi}** fiorygi.", - parse_mode="Markdown") - - -@catch_and_report -def cmd_bridge(bot: Bot, update: Update): try: data = update.message.text.split(" ", 1)[1] except IndexError: - bot.send_message(update.message.chat.id, - "⚠ Non hai specificato un comando!\n" - "Sintassi corretta: `/bridge `", - parse_mode="Markdown") + reply(bot, update, strings.BRIDGE.ERRORS.INVALID_SYNTAX) return discord_connection.send(f"!{data}") result = discord_connection.recv() if result == "error": - bot.send_message(update.message.chat.id, "⚠ Il comando specificato non esiste.") + reply(bot, update, strings.BRIDGE.FAILURE) if result == "success": - bot.send_message(update.message.chat.id, "⏩ Comando eseguito su Discord.") + reply(bot, update, strings.BRIDGE.SUCCESS) def parse_timestring(timestring: str) -> typing.Union[datetime.timedelta, datetime.datetime]: @@ -466,8 +634,9 @@ def parse_timestring(timestring: str) -> typing.Union[datetime.timedelta, dateti raise ValueError("Nothing was found.") -@catch_and_report -def cmd_newevent(bot: Bot, update: Update): +@command +@database_access +def cmd_newevent(bot: telegram.Bot, update: telegram.Update, session: db.Session): try: _, timestring, name_desc = update.message.text.split(" ", 2) except ValueError: @@ -499,7 +668,6 @@ def cmd_newevent(bot: Bot, update: Update): "per favore inserisci una data futura.\n", parse_mode="Markdown") return # Create the event - session = db.Session() telegram_user = session.query(db.Telegram)\ .filter_by(telegram_id=update.message.from_user.id)\ .join(db.Royal)\ @@ -516,15 +684,13 @@ def cmd_newevent(bot: Bot, update: Update): # Save the event session.add(event) session.commit() - session.close() bot.send_message(update.message.chat.id, "✅ Evento aggiunto al Calendario Royal Games!") -@catch_and_report -def cmd_calendar(bot: Bot, update: Update): - session = db.Session() +@command +@database_access +def cmd_calendar(bot: telegram.Bot, update: telegram.Update, session: db.Session): next_events = session.query(db.Event).filter(db.Event.time > datetime.datetime.now()).order_by(db.Event.time).all() - session.close() msg = "📆 Prossimi eventi\n" for event in next_events: if event.time_left.days >= 1: @@ -537,39 +703,128 @@ def cmd_calendar(bot: Bot, update: Update): bot.send_message(update.message.chat.id, msg, parse_mode="HTML", disable_web_page_preview=True) -@catch_and_report -def cmd_markov(bot: Bot, update: Update): - if model is None: - bot.send_message(update.message.chat.id, "⚠️ Il modello Markov non è disponibile.") +@command +def cmd_markov(bot: telegram.Bot, update: telegram.Update): + if default_model is None: + reply(bot, update, strings.MARKOV.ERRORS.NO_MODEL) return try: - _, first_word = update.message.text.split(" ", 1) - except ValueError: - sentence = model.make_sentence(tries=1000) + first_word = update.message.text.split(" ")[1] + except IndexError: + # Any word + sentence = default_model.make_sentence(tries=1000) if sentence is None: - bot.send_message(update.message.chat.id, "⚠ Complimenti! Hai vinto la lotteria di Markov!\n" - "O forse l'hai persa.\n" - "Non sono riuscito a generare una frase, riprova.") + reply(bot, update, strings.MARKOV.ERRORS.GENERATION_FAILED) return - bot.send_message(update.message.chat.id, sentence) + reply(bot, update, sentence) + return + # Specific word + try: + sentence = default_model.make_sentence_with_start(first_word, tries=1000) + except KeyError: + reply(bot, update, strings.MARKOV.ERRORS.MISSING_WORD) + return + if sentence is None: + reply(bot, update, strings.MARKOV.ERRORS.SPECIFIC_WORD_FAILED) + return + reply(bot, update, sentence) + + +@command +def cmd_dndmarkov(bot: telegram.Bot, update: telegram.Update): + if dnd4_model is None: + reply(bot, update, strings.MARKOV.ERRORS.NO_MODEL) + return + try: + first_word = update.message.text.split(" ")[1] + except IndexError: + # Any word + sentence = dnd4_model.make_sentence(tries=1000) + if sentence is None: + reply(bot, update, strings.MARKOV.ERRORS.GENERATION_FAILED) + return + reply(bot, update, sentence) + return + # Specific word + try: + sentence = dnd4_model.make_sentence_with_start(first_word, tries=1000) + except KeyError: + reply(bot, update, strings.MARKOV.ERRORS.MISSING_WORD) + return + if sentence is None: + reply(bot, update, strings.MARKOV.ERRORS.SPECIFIC_WORD_FAILED) + return + reply(bot, update, sentence) + + +def exec_roll(roll) -> str: + result = int(roll.evaluate()) + string = "" + if isinstance(roll, dice.elements.Dice): + string += f"{result}" else: - sentence = model.make_sentence_with_start(first_word, tries=1000) - if sentence is None: - bot.send_message(update.message.chat.id, "⚠ Non è stato possibile generare frasi partendo da questa" - " parola.") - return - bot.send_message(update.message.chat.id, sentence) + for index, operand in enumerate(roll.original_operands): + if operand != roll.operands[index]: + string += f"{roll.operands[index]}" + else: + string += f"{operand}" + if index + 1 != len(roll.original_operands): + + string += strings.ROLL.SYMBOLS[roll.__class__] + string += f"={result}" + return string -@catch_and_report -def cmd_roll(bot: Bot, update: Update): +@command +def cmd_roll(bot: telegram.Bot, update: telegram.Update): dice_string = update.message.text.split(" ", 1)[1] try: - result = dice.roll(f"{dice_string}t") + roll = dice.roll(f"{dice_string}", raw=True) except dice.DiceBaseException: - bot.send_message(update.message.chat.id, "⚠ Il tiro dei dadi è fallito. Controlla la sintassi!") + reply(bot, update, strings.ROLL.ERRORS.INVALID_SYNTAX) return - bot.send_message(update.message.chat.id, f"🎲 {result}") + try: + result = exec_roll(roll) + except dice.DiceFatalException: + reply(bot, update, strings.ROLL.ERRORS.DICE_ERROR) + return + reply(bot, update, strings.ROLL.SUCCESS, result=result, ignore_escaping=True) + + +@command +def cmd_start(bot: telegram.Bot, update: telegram.Update): + reply(bot, update, strings.TELEGRAM.BOT_STARTED) + + +@command +def cmd_spell(bot: telegram.Bot, update: telegram.Update): + try: + spell_name: str = update.message.text.split(" ", 1)[1] + except IndexError: + reply(bot, update, strings.SPELL.ERRORS.INVALID_SYNTAX) + return + spell = cast.Spell(spell_name) + reply(bot, update, spell.stringify()) + + +@command +def cmd_emojify(bot: telegram.Bot, update: telegram.Update): + try: + string: str = update.message.text.split(" ", 1)[1] + except IndexError: + reply(bot, update, strings.EMOJIFY.ERRORS.INVALID_SYNTAX) + return + msg = emojify(string) + reply(bot, update, strings.EMOJIFY.RESPONSE, emojified=msg) + + +@command +def cmd_pug(bot: telegram.Bot, update: telegram.Update): + if update.effective_chat.type != telegram.Chat.PRIVATE: + reply(bot, update, strings.PUG.ERRORS.PRIVATE_CHAT_ONLY) + return + j = requests.get("https://dog.ceo/api/breed/pug/images/random").json() + reply(bot, update, strings.PUG.HERE_HAVE_A_PUG, disable_web_page_preview=False, image_url=j["message"]) def process(arg_discord_connection): @@ -580,27 +835,40 @@ def process(arg_discord_connection): u = Updater(config["Telegram"]["bot_token"]) logger.info("Registering handlers...") u.dispatcher.add_handler(CommandHandler("ping", cmd_ping)) - u.dispatcher.add_handler(CommandHandler("register", cmd_register)) - u.dispatcher.add_handler(CommandHandler("discord", cmd_discord)) - u.dispatcher.add_handler(CommandHandler("cv", cmd_discord)) + u.dispatcher.add_handler(CommandHandler("pong", cmd_ping)) + u.dispatcher.add_handler(CommandHandler("link", cmd_link)) + u.dispatcher.add_handler(CommandHandler("discord", cmd_cv)) + u.dispatcher.add_handler(CommandHandler("cv", cmd_cv)) u.dispatcher.add_handler(CommandHandler("cast", cmd_cast)) u.dispatcher.add_handler(CommandHandler("color", cmd_color)) + u.dispatcher.add_handler(CommandHandler("error", cmd_color)) u.dispatcher.add_handler(CommandHandler("smecds", cmd_smecds)) u.dispatcher.add_handler(CommandHandler("ciaoruozi", cmd_ciaoruozi)) u.dispatcher.add_handler(CommandHandler("ahnonlosoio", cmd_ahnonlosoio)) u.dispatcher.add_handler(CommandHandler("balurage", cmd_balurage)) u.dispatcher.add_handler(CommandHandler("diario", cmd_diario)) u.dispatcher.add_handler(CommandHandler("spaggia", cmd_diario)) + u.dispatcher.add_handler(CommandHandler("spaggio", cmd_diario)) u.dispatcher.add_handler(CommandHandler("vote", cmd_vote)) u.dispatcher.add_handler(CommandHandler("eat", cmd_eat)) u.dispatcher.add_handler(CommandHandler("ship", cmd_ship)) - u.dispatcher.add_handler(CommandHandler("profile", cmd_profile)) u.dispatcher.add_handler(CommandHandler("bridge", cmd_bridge)) u.dispatcher.add_handler(CommandHandler("newevent", cmd_newevent)) u.dispatcher.add_handler(CommandHandler("calendar", cmd_calendar)) u.dispatcher.add_handler(CommandHandler("markov", cmd_markov)) + u.dispatcher.add_handler(CommandHandler("dndmarkov", cmd_dndmarkov)) u.dispatcher.add_handler(CommandHandler("roll", cmd_roll)) u.dispatcher.add_handler(CommandHandler("r", cmd_roll)) + u.dispatcher.add_handler(CommandHandler("mm", cmd_mm)) + u.dispatcher.add_handler(CommandHandler("matchmaking", cmd_mm)) + u.dispatcher.add_handler(CommandHandler("search", cmd_search)) + u.dispatcher.add_handler(CommandHandler("regex", cmd_regex)) + u.dispatcher.add_handler(CommandHandler("start", cmd_start)) + u.dispatcher.add_handler(CommandHandler("spell", cmd_spell)) + u.dispatcher.add_handler(CommandHandler("emojify", cmd_emojify)) + u.dispatcher.add_handler(CommandHandler("pug", cmd_pug)) + u.dispatcher.add_handler(CommandHandler("carlino", cmd_pug)) + u.dispatcher.add_handler(CommandHandler("carlini", cmd_pug)) u.dispatcher.add_handler(CallbackQueryHandler(on_callback_query)) logger.info("Handlers registered.") u.start_polling() diff --git a/templates/400.html b/templates/400.html index d04d82c6..1b882e98 100644 --- a/templates/400.html +++ b/templates/400.html @@ -11,4 +11,14 @@

Il tuo browser ha inviato una richiesta non valida. Magari non hai riempito qualche campo di un form?

+
+
+

+ Villa di Von Shdfisjz +

+

+ Steffo, 2017-07-26 18:46:43 +

+
+
{% endblock %} \ No newline at end of file diff --git a/templates/403.html b/templates/403.html index bfc94b22..4d2aafea 100644 --- a/templates/403.html +++ b/templates/403.html @@ -9,6 +9,25 @@ 403 - Forbidden

- Non puoi accedere a questa pagina. Magari hai sbagliato password? + Non puoi accedere a questa pagina.

+ {% if g.logged_in %} +

+ Forse dovresti provare a fare il login... +

+ {% else %} +

+ Temo che questa pagina sia riservata agli amministratori... +

+ {% endif %} +
+
+

+ Io sono il padrone, questo è champagne, buon Natale! +

+

+ Anonimo, 2017-02-10 09:11:00 +

+
+
{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html index 34fb14dd..f887d27b 100644 --- a/templates/500.html +++ b/templates/500.html @@ -11,14 +11,18 @@

Il server è crashato mentre cercava di generare questa pagina. Oops.

-
-

- I am sorry, unknown error occured during working with your request, Admin were notified -

+
+
+

+ I am sorry, unknown error occured during working with your request, Admin were notified +

+

+ OcteonRygBot, 2017-09-14 14:11:00 +

+

L'errore dovrebbe essere stato segnalato a Steffo. - Se riesci e ne hai voglia, spiegagli anche cosa ha provocato l'errore. + Se riesci e ne hai voglia, spiegagli cosa ha provocato l'errore.

- @OcteonRygBot, 2017 {% endblock %} \ No newline at end of file diff --git a/templates/activity.html b/templates/activity.html index 1fdbf3ab..7b4054d1 100644 --- a/templates/activity.html +++ b/templates/activity.html @@ -18,7 +18,7 @@

Attività su Discord negli ultimi 7 giorni

- + - + - + +

+ Attività per ogni ora nell'ultimo mese +

+ + +

+ Confronto cv con il mese scorso +

+ + diff --git a/templates/base.html b/templates/base.html index cfa63c9f..9c943137 100644 --- a/templates/base.html +++ b/templates/base.html @@ -14,7 +14,7 @@ - {% block body %}{% endblock %} +
+ {% block body %}{% endblock %} +
\ No newline at end of file diff --git a/templates/components/diarioentry.html b/templates/components/diarioentry.html index caa95470..a9109374 100644 --- a/templates/components/diarioentry.html +++ b/templates/components/diarioentry.html @@ -1,13 +1,23 @@ -
+

{{ entry.to_html() | safe }}

- — {% if entry.author is not none %}{{ entry.author.royal.username }}{% else %}Anonimo{% endif %}, {{ entry.timestamp.strftime('%Y-%m-%d %H:%M:%S %Z') }} {% if entry.saver is not none and entry.saver != entry.author %}(salvato da {{ entry.saver.royal.username }}){% endif %} + — + {% if entry.author is not none %} + {{ entry.author.royal.username }} + {% else %} + Anonimo + {% endif %}, + {{ entry.timestamp.strftime('%Y-%m-%d %H:%M:%S %Z') }} + {% if entry.saver is not none and entry.saver != entry.author %} + (salvato da {{ entry.saver.royal.username }}) + {% endif %} +

\ No newline at end of file diff --git a/templates/components/diariooftheday.html b/templates/components/diariooftheday.html new file mode 100644 index 00000000..4fd29399 --- /dev/null +++ b/templates/components/diariooftheday.html @@ -0,0 +1,9 @@ +
+
+ Citazione casuale dal diario +
+
+ {% include "components/diarioentry.html" %} + Visualizza tutto il diario +
+
\ No newline at end of file diff --git a/templates/components/eventlist.html b/templates/components/eventlist.html new file mode 100644 index 00000000..e8f2d779 --- /dev/null +++ b/templates/components/eventlist.html @@ -0,0 +1,10 @@ +
+
+ Prossimi eventi +
+
+ {% for event in events %} + {% include "components/event.html" %} + {% endfor %} +
+
\ No newline at end of file diff --git a/templates/components/gamestatsbox.html b/templates/components/gamestatsbox.html new file mode 100644 index 00000000..596150b9 --- /dev/null +++ b/templates/components/gamestatsbox.html @@ -0,0 +1,19 @@ +
+
+ Noi in vari giochi +
+ +
\ No newline at end of file diff --git a/templates/components/links.html b/templates/components/links.html new file mode 100644 index 00000000..c15788a9 --- /dev/null +++ b/templates/components/links.html @@ -0,0 +1,26 @@ +
+
+ Link riservati ai membri +
+
+ +
+
\ No newline at end of file diff --git a/templates/components/memberbox.html b/templates/components/memberbox.html new file mode 100644 index 00000000..6fdf9996 --- /dev/null +++ b/templates/components/memberbox.html @@ -0,0 +1,12 @@ +
+
+ Membri +
+
+ +
+
\ No newline at end of file diff --git a/templates/components/quest.html b/templates/components/quest.html new file mode 100644 index 00000000..60769068 --- /dev/null +++ b/templates/components/quest.html @@ -0,0 +1,14 @@ +
+
+ {{ quest.title }} +
+
+ {{ quest.description | markdown }} +
+
+ {% if quest.reward %} +
{{ quest.reward }}
+
fioryg{% if quest.reward != 1 %}i{% endif %}
+ {% endif %} +
+
\ No newline at end of file diff --git a/templates/components/questboard.html b/templates/components/questboard.html new file mode 100644 index 00000000..8ff19329 --- /dev/null +++ b/templates/components/questboard.html @@ -0,0 +1,14 @@ +
+
+ Tabellone delle quest +
+
+ {% if quests %} + {% for quest in quests %} + {% include "components/quest.html" %} + {% endfor %} + {% else %} + Non ci sono quest al momento. + {% endif %} +
+
\ No newline at end of file diff --git a/templates/components/welcome.html b/templates/components/welcome.html new file mode 100644 index 00000000..dfec0ee8 --- /dev/null +++ b/templates/components/welcome.html @@ -0,0 +1,8 @@ +
+
+ Benvenuti! +
+
+ Benvenuti al sito della community Royal Games! +
+
\ No newline at end of file diff --git a/templates/components/whatarewe.html b/templates/components/whatarewe.html new file mode 100644 index 00000000..2fd6df5b --- /dev/null +++ b/templates/components/whatarewe.html @@ -0,0 +1,9 @@ +
+
+ Cosa siamo? +
+
+ La Royal Games è una community di gamer di tutti i tipi che sono capitati insieme più o meno per caso, e continuano a trovarsi spesso online per parlare e giocare insieme.
+ Essendo nata parecchi anni fa, all'interno di essa si è sviluppata una particolare cultura che la rende unica in tutto l'Internet. +
+
\ No newline at end of file diff --git a/templates/components/wikibox.html b/templates/components/wikibox.html index 79c9d4f6..6e90528e 100644 --- a/templates/components/wikibox.html +++ b/templates/components/wikibox.html @@ -9,11 +9,13 @@ Pagine Wiki
-
    + - oppure... + {% if g.user %} + oppure... + {% endif %}
\ No newline at end of file diff --git a/templates/diario.html b/templates/diario.html index 1881632d..469c1e9b 100644 --- a/templates/diario.html +++ b/templates/diario.html @@ -12,11 +12,9 @@

Diario Cos'è?

-
-
- {% for entry in entries %} - {% include "components/diarioentry.html" %} - {% endfor %} -
+
+ {% for entry in entries %} + {% include "components/diarioentry.html" %} + {% endfor %}
{% endblock %} \ No newline at end of file diff --git a/templates/game.html b/templates/game.html index d39acbd0..c450d8ae 100644 --- a/templates/game.html +++ b/templates/game.html @@ -6,22 +6,23 @@ {% endblock %} {% block pagetitle %} - {{ game_name }} + {{ mini_type._mini_full_name }} {% endblock %} {% block body %}

- Royal Games su {{ game_name }} + Royal Games su {{ mini_type._mini_full_name }}

-
-
-
- {% for mini in minis %} - {% with record = mini %} - {% include "minis/" + game_short_name + ".html" %} - {% endwith %} - {% endfor %} -
+ {% if mini_type._mini_name == "ow" %} +
+ Overwatch updates are currently disabled. +
+ {% endif %} +
+
+ {% for record in mini_data %} + {% include "minis/" + mini_type._mini_name + ".html" %} + {% endfor %}
{% endblock %} \ No newline at end of file diff --git a/templates/main.html b/templates/main.html index 26606b02..2be4af1d 100644 --- a/templates/main.html +++ b/templates/main.html @@ -9,90 +9,36 @@ {% endblock %} {% block body %} -

- Royal Games +

+ Royal Games

-
+
-
- {% if next_events %} -
- Prossimi eventi -
-
- {% for event in next_events %} - {% include "components/event.html" %} - {% endfor %} -
+ {% if not g.user %} + {% include "components/welcome.html" %} + {% endif %} + {% if g.user %} + {% if events %} + {% include "components/eventlist.html" %} {% endif %} -
-
-
- Citazione casuale dal diario -
-
- {% include "components/diarioentry.html" %} - Visualizza tutto -
-
- {% include "components/wikibox.html" %} + {% endif %} + {% include "components/memberbox.html" %} + {% if g.user %} + {% include "components/wikibox.html" %} + {% endif %}
-
-
- Link utili -
-
- -
-
-
-
- Membri -
-
- -
-
-
-
- Resoconti -
- -
+ {% if not g.user %} + {% include "components/whatarewe.html" %} + {% endif %} + {% if g.user %} + {% include "components/questboard.html" %} + {% endif %} + {% include "components/gamestatsbox.html" %} + {% if g.user %} + {% include "components/links.html" %} + {% include "components/diariooftheday.html" %} + {% endif %}
{% endblock %} \ No newline at end of file diff --git a/templates/minis/dota.html b/templates/minis/dota.html index 94587041..bc9dcb8c 100644 --- a/templates/minis/dota.html +++ b/templates/minis/dota.html @@ -21,11 +21,14 @@ {{ record.wins }}
- {% if record.rank_tier is none %} - + {% if (record.wins + record.losses) < 100 %} + + {{ 100 - record.wins - record.losses }} partite rimaste + {% elif record.rank_tier is none %} + Non classificato {% elif record.rank_tier < 10 %} - + {{ record.rank_tier }} piazzamenti completati {% else %} {% if record.get_rank_stars_url() %}{% endif %} diff --git a/templates/minis/ryg.html b/templates/minis/ryg.html index 0fde5b94..35b78f17 100644 --- a/templates/minis/ryg.html +++ b/templates/minis/ryg.html @@ -1,16 +1,64 @@
-
- -
- {{ record.role }} dal {% if record.member_since %}{{ record.member_since }}{% else %}????-??-??{% endif %} -
-
- Fiorygi -
-
- {{ record.fiorygi }} +
+ + {% if record.special_title %} +
+ {{ record.special_title }} +
+ {% endif %} + {% if record.username == "Steffo" %} +
+ Admin dall' +
+
+ inizio! +
+ {% elif record.member_since %} +
+ {{ record.role }} dal +
+
+ {{ record.member_since.isoformat() }} +
+ {% else %} +
+ {{ record.role }} dal +
+
+ ? +
+ {% endif %} + {% if record.wiki_edits|length > 0 %} +
+ Contributi wiki +
+
+ {{ record.wiki_edits|length }} +
+ {% else %} +
+ Contributi wiki +
+
+ --- +
+ {% endif %} + {% if record.fiorygi > 0 %} +
+ Fiorygi +
+
+ {{ record.fiorygi }} +
+ {% else %} +
+ Fiorygi +
+
+ --- +
+ {% endif %}
diff --git a/templates/minis/terraria13.html b/templates/minis/terraria13.html new file mode 100644 index 00000000..a922fbe7 --- /dev/null +++ b/templates/minis/terraria13.html @@ -0,0 +1,21 @@ + +
+
+
+ {{ record.character_name }} +
+
+ + {{ record.contribution }} + + fioryg{% if record.contribution != 1 %}i{% endif %} ottenut{% if record.contribution != 1 %}i{% else %}o{% endif %} per la partecipazione a Terraria 13! +
+
+
diff --git a/templates/profile.html b/templates/profile.html index 3541b844..04d15f24 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -23,63 +23,23 @@

Profilo di {{ ryg.username }} {% if session.get('user_id', '') == ryg.id %}Modifica{% endif %}

-
-
- {% if css.bio %} -
-
- Bio -
-
- {{ bio }} -
+
+ {% if css.bio %} +
+
+ Bio +
+
+ {{ bio }}
- {% endif %} -
- {% with record = ryg %} - {% include "minis/ryg.html" %} - {% endwith %} - {% if halloween %} - {% with record = halloween %} - {% include "minis/halloween2018.html" %} - {% endwith %} - {% endif %} - {% if tg %} - {% with record = tg %} - {% include "minis/tg.html" %} - {% endwith %} - {% endif %} - {% if discord %} - {% with record = discord %} - {% include "minis/discord.html" %} - {% endwith %} - {% endif %} - {% if steam %} - {% with record = steam %} - {% include "minis/steam.html" %} - {% endwith %} - {% endif %} - {% if dota %} - {% with record = dota %} - {% include "minis/dota.html" %} - {% endwith %} - {% endif %} - {% if osu %} - {% with record = osu %} - {% include "minis/osu.html" %} - {% endwith %} - {% endif %} - {% if lol %} - {% with record = lol %} - {% include "minis/lol.html" %} - {% endwith %} - {% endif %} - {% if ow %} - {% with record = ow %} - {% include "minis/ow.html" %} - {% endwith %} - {% endif %}
+ {% endif %} +
+ {% for mini in mini_data %} + {% with record = mini["data"] %} + {% include "minis/" + mini["name"] + ".html" %} + {% endwith %} + {% endfor %}
{% endblock %} \ No newline at end of file diff --git a/templates/profileedit.html b/templates/profileedit.html index 898cbe8d..2b87044c 100644 --- a/templates/profileedit.html +++ b/templates/profileedit.html @@ -9,6 +9,7 @@ Modifica profilo
+
Puoi usare il Markdown nella tua bio. Se vuoi, puoi personalizzare il tuo profilo con un tuo foglio di stile! diff --git a/templates/wikilist.html b/templates/wikilist.html deleted file mode 100644 index b6831caa..00000000 --- a/templates/wikilist.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'base.html' %} - -{% block pagetitle %} - Elenco delle Wiki -{% endblock %} - -{% block body %} -

- Royal Wiki -

-
- {% include "components/wikibox.html" %} -
-{% endblock %} \ No newline at end of file diff --git a/templates/wikipage.html b/templates/wikipage.html index 5b4e22ca..9487d24b 100644 --- a/templates/wikipage.html +++ b/templates/wikipage.html @@ -43,15 +43,37 @@ Ultima modifica di {{ wiki_log.editor.username }} alle {{ wiki_log.timestamp.strftime('%Y-%m-%d %H:%M:%S %Z') }}{% if wiki_log.reason %}, motivo: {{ wiki_log.reason }}{% endif %}
{% endif %} - {% if session.get('user_id', '') %} + {% if wiki_page is none %}
-

Modifica

- +

Crea

+
-
- +
+
+ {% elif wiki_page.locked %} +
+ 🔒 Pagina bloccata: non possono essere effettuate ulteriori modifiche. +
+ {% else %} + {% if g.user %} +
+

Modifica

+
+
+
+ +
+
+ {% endif %} + {% endif %} + {% if wiki_page is not none %} + {% if g.user.role == "Admin" %} +
+ +
+ {% endif %} {% endif %}
{% endblock %} \ No newline at end of file diff --git a/update.sh b/update.sh index 70331006..ff540b70 100755 --- a/update.sh +++ b/update.sh @@ -2,13 +2,15 @@ # Requires SENTRY_AUTH_TOKEN and SENTRY_ORG set in .sentryclirc old=$(git rev-list HEAD -n 1) +git stash git pull +git lfs pull new=$(git rev-list HEAD -n 1) -if [ ${old} = ${new} ]; then +if [[ ${old} = ${new} ]]; then version=$(sentry-cli releases propose-version) sentry-cli releases new -p royalnet ${version} sentry-cli releases set-commits --auto ${version} fi -sudo python3.6 -m pip install -r requirements.txt +sudo python3.6 -m pip install -r requirements.txt --upgrade sudo service apache2 restart python3.6 -OO bots.py diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 00000000..38bc0ae9 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,8 @@ +from .dirty import Dirty, DirtyDelta +from .mmstatus import MatchmakingStatus +from .cast import Spell +from .stagismo import smecds +from .emojify import emojify +from .telegramstuff import reply_msg + +__all__ = ["Dirty", "DirtyDelta", "MatchmakingStatus", "Spell", "smecds", "emojify", "reply_msg"] diff --git a/utils/cast.py b/utils/cast.py new file mode 100644 index 00000000..72a1b030 --- /dev/null +++ b/utils/cast.py @@ -0,0 +1,230 @@ +import random +import math +import typing +import strings +import enum +s = strings.safely_format_string + + +class SpellType(enum.Flag): + DAMAGING = enum.auto() + HEALING = enum.auto() + STATS = enum.auto() + STATUS_EFFECT = enum.auto() + + +class DamageComponent: + dice_type_distribution = ([4] * 7) +\ + ([6] * 12) +\ + ([8] * 32) +\ + ([10] * 30) +\ + ([12] * 12) +\ + ([20] * 6) +\ + ([100] * 1) + + all_damage_types = ["da fuoco", "da freddo", "elettrici", "sonici", "necrotici", "magici", + "da acido", "divini", "nucleari", "psichici", "fisici", "puri", "da taglio", + "da perforazione", "da impatto", "da caduta", "gelato", "onnipotenti", "oscuri", + "di luce", "da velocità", "da cactus", "dannosi", "da radiazione", + "tuamammici", "da maledizione", "pesanti", "leggeri", "immaginari", "da laser", + "da neutrini", "galattici", "cerebrali", "ritardati", "ritardanti", "morali", "materiali", + "energetici", "esplosivi", "energetici", "finanziari", "radianti", "sonori", "spaggiaritici", + "interiori", "endocrini", "invisibili", "inesistenti", "eccellenti", "bosonici", + "gellificanti", "terminali"] + + repeat_distribution = ([1] * 8) +\ + ([2] * 1) +\ + ([3] * 1) + + damage_types_distribution = ([1] * 6) + \ + ([2] * 3) + \ + ([3] * 1) + + def __init__(self): + # ENSURE THE SEED IS ALREADY SET WHEN CREATING THIS COMPONENT!!! + self.dice_number = random.randrange(1, 21) + self.dice_type = random.sample(self.dice_type_distribution, 1)[0] + self.constant = random.randrange(math.floor(-self.dice_type / 4), math.ceil(self.dice_type / 4) + 1) + self.miss_chance = random.randrange(50, 101) + self.repeat = random.sample(self.repeat_distribution, 1)[0] + self.damage_types_qty = random.sample(self.damage_types_distribution, 1)[0] + self.damage_types = random.sample(self.all_damage_types, self.damage_types_qty) + + @property + def avg(self): + return (self.dice_number * (self.dice_type + 1) / 2) + self.constant + + def stringify(self) -> str: + string = "" + if self.constant > 0: + constant = "+" + str(self.constant) + elif self.constant == 0: + constant = "" + else: + constant = str(self.constant) + string += s(strings.SPELL.DAMAGE, + words={"number": str(self.dice_number), + "type": str(self.dice_type), + "constant": constant, + "avg": str(int(self.avg))}) + for dmg_type in self.damage_types: + string += s(strings.SPELL.TYPE, words={"type": dmg_type}) + string += s(strings.SPELL.ACCURACY, words={"accuracy": str(self.miss_chance)}) + if self.repeat > 1: + string += s(strings.SPELL.REPEAT, words={"repeat": str(self.repeat)}) + return string + + +class HealingComponent: + dice_type_distribution = ([4] * 12) +\ + ([6] * 38) +\ + ([8] * 30) +\ + ([10] * 12) +\ + ([12] * 6) +\ + ([20] * 1) +\ + ([100] * 1) + + def __init__(self): + # ENSURE THE SEED IS ALREADY SET WHEN CREATING THIS COMPONENT!!! + self.dice_number = random.randrange(1, 11) + self.dice_type = random.sample(self.dice_type_distribution, 1)[0] + self.constant = random.randrange(math.floor(-self.dice_type / 4), math.ceil(self.dice_type / 4) + 1) + + @property + def avg(self): + return (self.dice_number * (self.dice_type + 1) / 2) + self.constant + + def stringify(self) -> str: + string = "" + if self.constant > 0: + constant = "+" + str(self.constant) + elif self.constant == 0: + constant = "" + else: + constant = str(self.constant) + string += s(strings.SPELL.HEALING, + words={"number": str(self.dice_number), + "type": str(self.dice_type), + "constant": constant, + "avg": str(int(self.avg))}) + return string + + +class StatsComponent: + all_stats = ["Attacco", "Difesa", "Velocità", "Elusione", "Tenacia", "Rubavita", + "Vampirismo", "Forza", "Destrezza", "Costituzione", "Intelligenza", + "Saggezza", "Carisma", "Attacco Speciale", "Difesa Speciale", + "Eccellenza", "Immaginazione", "Cromosomi", "Timidezza", "Sonno", + "Elasticità", "Peso", "Sanità", "Appetito", "Fortuna", "Percezione", + "Determinazione"] + + change_distribution = (["--"] * 1) +\ + (["-"] * 2) +\ + (["+"] * 2) +\ + (["++"] * 1) + + multistat_distribution = ([1] * 16) +\ + ([2] * 8) +\ + ([3] * 4) +\ + ([5] * 2) +\ + ([8] * 1) + + def __init__(self): + # ENSURE THE SEED IS ALREADY SET WHEN CREATING THIS COMPONENT!!! + self.stat_changes = {} + self.stat_number = random.sample(self.multistat_distribution, 1)[0] + available_stats = self.all_stats.copy() + for _ in range(self.stat_number): + stat = random.sample(available_stats, 1)[0] + available_stats.remove(stat) + change = random.sample(self.change_distribution, 1)[0] + self.stat_changes[stat] = change + + def stringify(self) -> str: + string = "" + for name in self.stat_changes: + string += s(strings.SPELL.STAT, words={ + "name": name, + "change": self.stat_changes[name] + }) + return string + + +class StatusEffectComponent: + all_status_effects = ["Bruciatura", "Sanguinamento", "Paralisi", "Veleno", + "Congelamento", "Iperveleno", "Sonno", "Stordimento", + "Rallentamento", "Radicamento", "Rigenerazione", "Morte", + "Affaticamento", "Glitch", "Accecamento", "Silenzio", + "Esilio", "Invisibilità", "Rapidità", "Splendore"] + + def __init__(self): + # ENSURE THE SEED IS ALREADY SET WHEN CREATING THIS COMPONENT!!! + self.chance = random.randrange(1, 101) + self.effect = random.sample(self.all_status_effects, 1)[0] + + def stringify(self) -> str: + return s(strings.SPELL.STATUS_EFFECT, words={ + "chance": str(self.chance), + "effect": self.effect + }) + + + +class Spell: + version = "3.2" + + damaging_spell_chance = 0.9 + healing_spell_chance = 0.9 # If not a damaging spell + additional_stats_chance = 0.1 # In addition to the damage/healing + additional_status_effect_chance = 0.1 # In addition to the rest + + def __init__(self, name: str): + seed = name.capitalize() + random.seed(seed) + # Spell name + self.name = seed + # Find the spell type + self.spell_type = SpellType(0) + if random.random() < self.damaging_spell_chance: + self.spell_type |= SpellType.DAMAGING + elif random.random() < self.healing_spell_chance: + self.spell_type |= SpellType.HEALING + if random.random() < self.additional_stats_chance: + self.spell_type |= SpellType.STATS + if random.random() < self.additional_status_effect_chance: + self.spell_type |= SpellType.STATUS_EFFECT + # Damaging spells + if self.spell_type & SpellType.DAMAGING: + self.damage_component = DamageComponent() + else: + self.damage_component = None + # Healing spells + if self.spell_type & SpellType.HEALING: + self.healing_component = HealingComponent() + else: + self.healing_component = None + # Stats spells + if self.spell_type & SpellType.STATS: + self.stats_component = StatsComponent() + else: + self.stats_component = None + # Status effect spells + if self.spell_type & SpellType.STATUS_EFFECT: + self.status_effect_component = StatusEffectComponent() + else: + self.status_effect_component = None + + + def stringify(self) -> str: + string = s(strings.SPELL.HEADER, words={"name": self.name, "version": self.version}) + if self.spell_type & SpellType.DAMAGING: + string += self.damage_component.stringify() + if self.spell_type & SpellType.HEALING: + string += self.healing_component.stringify() + if self.spell_type & SpellType.STATS: + string += self.stats_component.stringify() + if self.spell_type & SpellType.STATUS_EFFECT: + string += self.status_effect_component.stringify() + if self.spell_type == SpellType(0): + string += s(strings.SPELL.NOTHING) + return string diff --git a/utils/dirty.py b/utils/dirty.py new file mode 100644 index 00000000..42123db1 --- /dev/null +++ b/utils/dirty.py @@ -0,0 +1,37 @@ +class Dirty: + def __init__(self, initial_value): + self.initial_value = initial_value + self.value = initial_value + + def is_clean(self): + return self.initial_value == self.value + + def is_dirty(self): + return not self.is_clean() + + def __bool__(self): + return self.is_dirty() + + +class DirtyDelta(Dirty): + + @property + def difference(self): + if self.initial_value is None: + initial_value = 0 + else: + initial_value = self.initial_value + if self.value is None: + value = 0 + else: + value = self.value + return value - initial_value + + @property + def delta(self): + return abs(self.difference) + + def difference_string(self): + if self.difference > 0: + return f"+{self.difference}" + return self.difference diff --git a/utils/emojify.py b/utils/emojify.py new file mode 100644 index 00000000..b25fd194 --- /dev/null +++ b/utils/emojify.py @@ -0,0 +1,59 @@ +import random + +emojis = { + "abcd": ["🔡", "🔠"], + "back": ["🔙"], + "cool": ["🆒"], + "free": ["🆓"], + "abc": ["🔤"], + "atm": ["🏧"], + "new": ["🆕"], + "sos": ["🆘"], + "top": ["🔝"], + "zzz": ["💤"], + "end": ["🔚"], + "ab": ["🆎"], + "cl": ["🆑"], + "id": ["🆔"], + "ng": ["🆖"], + "no": ["♑️"], + "ok": ["🆗"], + "on": ["🔛"], + "sy": ["💱"], + "tm": ["™️"], + "wc": ["🚾"], + "up": ["🆙"], + "a": ["🅰️"], + "b": ["🅱️"], + "c": ["☪️", "©", "🥐"], + "d": ["🇩"], + "e": ["📧", "💶"], + "f": ["🎏"], + "g": ["🇬"], + "h": ["🏨", "🏩", "🏋‍♀", "🏋‍♂"], + "i": ["ℹ️", "♊️", "🕕"], + "j": ["⤴️"], + "k": ["🎋", "🦅", "💃"], + "l": ["🛴", "🕒"], + "m": ["♏️", "Ⓜ️", "〽️"], + "n": ["📈"], + "o": ["⭕️", "🅾️", "📯", "🌝", "🌚", "🌕", "🥯", "🙆‍♀", "🙆‍♂"], + "p": ["🅿️"], + "q": ["🔍", "🍀"], + "r": ["®"], + "s": ["💰", "💵", "💸", "💲"], + "t": ["✝️", "⬆️", "☦️"], + "u": ["⛎", "⚓️", "🍉", "🌙", "🐋"], + "v": ["✅", "🔽", "☑️", "✔️"], + "w": ["🤷‍♀","🤷‍♂", "🤾‍♀", "🤾‍♂", "🤽‍♀", "🤽‍♂"], + "x": ["🙅‍♀", "🙅‍♂", "❌", "❎"], + "y": ["💴"], + "z": ["⚡️"] +} + +def emojify(string: str): + new_string = string.lower() + for key in emojis: + selected_emoji = random.sample(emojis[key], 1)[0] + new_string = new_string.replace(key, selected_emoji) + return new_string \ No newline at end of file diff --git a/errors.py b/utils/errors.py similarity index 100% rename from errors.py rename to utils/errors.py diff --git a/utils/mmstatus.py b/utils/mmstatus.py new file mode 100644 index 00000000..8a308605 --- /dev/null +++ b/utils/mmstatus.py @@ -0,0 +1,9 @@ +import enum + + +class MatchmakingStatus(enum.IntEnum): + WAIT_FOR_ME = 1 + READY = 2 + MAYBE = 3 + SOMEONE_ELSE = 4 + IGNORED = -1 diff --git a/utils/stagismo.py b/utils/stagismo.py new file mode 100644 index 00000000..71b46885 --- /dev/null +++ b/utils/stagismo.py @@ -0,0 +1,46 @@ +smecds = ["della secca", "del seccatore", "del secchiello", "del secchio", "del secchione", "del secondino", + "del sedano", "del sedativo", "della sedia", "del sedicente", "del sedile", "della sega", "del segale", + "della segatura", "della seggiola", "del seggiolino", "della seggiovia", "della segheria", "del seghetto", + "del segnalibro", "del segnaposto", "del segno", "del segretario", "della segreteria", "del seguace", + "del segugio", "della selce", "della sella", "della selz", "della selva", "della selvaggina", "del semaforo", + "del seme", "del semifreddo", "del seminario", "della seminarista", "della semola", "del semolino", + "del semplicione", "della senape", "del senatore", "del seno", "del sensore", "della sentenza", + "della sentinella", "del sentore", "della seppia", "del sequestratore", "della serenata", "del sergente", + "del sermone", "della serpe", "del serpente", "della serpentina", "della serra", "del serraglio", + "del serramanico", "della serranda", "della serratura", "del servitore", "della servitù", "del servizievole", + "del servo", "del set", "della seta", "della setola", "del sidecar", "del siderurgico", "del sidro", + "della siepe", "del sifone", "della sigaretta", "del sigaro", "del sigillo", "della signora", + "della signorina", "del silenziatore", "della silhouette", "del silicio", "del silicone", "del siluro", + "della sinagoga", "della sindacalista", "del sindacato", "del sindaco", "della sindrome", "della sinfonia", + "del sipario", "del sire", "della sirena", "della siringa", "del sismografo", "del sobborgo", + "del sobillatore", "del sobrio", "del soccorritore", "del socio", "del sociologo", "della soda", "del sofà", + "della soffitta", "del software", "dello sogghignare", "del soggiorno", "della sogliola", "del sognatore", + "della soia", "del solaio", "del solco", "del soldato", "del soldo", "del sole", "della soletta", + "della solista", "del solitario", "del sollazzare", "del sollazzo", "del sollecito", "del solleone", + "del solletico", "del sollevare", "del sollievo", "del solstizio", "del solubile", "del solvente", + "della soluzione", "del somaro", "del sombrero", "del sommergibile", "del sommo", "della sommossa", + "del sommozzatore", "del sonar", "della sonda", "del sondaggio", "del sondare", "del sonnacchioso", + "del sonnambulo", "del sonnellino", "del sonnifero", "del sonno", "della sonnolenza", "del sontuoso", + "del soppalco", "del soprabito", "del sopracciglio", "del sopraffare", "del sopraffino", "del sopraluogo", + "del sopramobile", "del soprannome", "del soprano", "del soprappensiero", "del soprassalto", + "del soprassedere", "del sopravvento", "del sopravvivere", "del soqquadro", "del sorbetto", "del sordido", + "della sordina", "del sordo", "della sorella", "della sorgente", "del sornione", "del sorpasso", + "della sorpresa", "del sorreggere", "del sorridere", "della sorsata", "del sorteggio", "del sortilegio", + "del sorvegliante", "del sorvolare", "del sosia", "del sospettoso", "del sospirare", "della sosta", + "della sostanza", "del sostegno", "del sostenitore", "del sostituto", "del sottaceto", "della sottana", + "del sotterfugio", "del sotterraneo", "del sottile", "del sottilizzare", "del sottintendere", + "del sottobanco", "del sottobosco", "del sottomarino", "del sottopassaggio", "del sottoposto", + "del sottoscala", "della sottoscrizione", "del sottostare", "del sottosuolo", "del sottotetto", + "del sottotitolo", "del sottovalutare", "del sottovaso", "della sottoveste", "del sottovuoto", + "del sottufficiale", "della soubrette", "del souvenir", "del soverchiare", "del sovrano", "del sovrapprezzo", + "della sovvenzione", "del sovversivo", "del sozzo", "dello suadente", "del sub", "del subalterno", + "del subbuglio", "del subdolo", "del sublime", "del suburbano", "del successore", "del succo", + "della succube", "del succulento", "della succursale", "del sudario", "della sudditanza", "del suddito", + "del sudicio", "del suffisso", "del suffragio", "del suffumigio", "del suggeritore", "del sughero", + "del sugo", "del suino", "della suite", "del sulfureo", "del sultano", "di Steffo", "di Spaggia", + "di Sabrina", "del sas", "del ses", "del sis", "del sos", "del sus", "della supremazia", "del Santissimo", + "della scatola", "del supercalifragilistichespiralidoso", "del sale", "del salame", "di (Town of) Salem", + "di Stronghold", "di SOMA", "dei Saints", "di S.T.A.L.K.E.R.", "di Sanctum", "dei Sims", "di Sid", + "delle Skullgirls", "di Sonic", "di Spiral (Knights)", "di Spore", "di Starbound", "di SimCity", "di Sensei", + "di Ssssssssssssss... Boom! E' esploso il dizionario", "della scala", "di Sakura", "di Suzie", "di Shinji", + "del senpai", "del support", "di Superman"] diff --git a/utils/telegramstuff.py b/utils/telegramstuff.py new file mode 100644 index 00000000..2cf4aa68 --- /dev/null +++ b/utils/telegramstuff.py @@ -0,0 +1,9 @@ +import telegram +import strings + + +def reply_msg(bot: telegram.Bot, chat_id: int, string: str, ignore_escaping=False, disable_web_page_preview=True, **kwargs) -> telegram.Message: + string = strings.safely_format_string(string, ignore_escaping=ignore_escaping, words=kwargs) + return bot.send_message(chat_id, string, + parse_mode="HTML", + disable_web_page_preview=disable_web_page_preview) diff --git a/webserver.py b/webserver.py index 066c49ad..ba82eba7 100644 --- a/webserver.py +++ b/webserver.py @@ -1,4 +1,3 @@ -import secrets from flask import Flask, render_template, request, abort, redirect, url_for, Markup, escape, jsonify from flask import session as fl_session from flask import g as fl_g @@ -9,10 +8,13 @@ import markdown2 import datetime # noinspection PyPackageRequirements import telegram -import query_discord_music -import random +import sql_queries import re +import functools +import strings +from sqlalchemy.orm.collections import InstrumentedList from raven.contrib.flask import Sentry +from utils import reply_msg app = Flask(__name__) @@ -29,9 +31,42 @@ telegram_bot = telegram.Bot(config["Telegram"]["bot_token"]) sentry = Sentry(app, dsn=config["Sentry"]["token"]) +@app.template_filter() +def markdown(text): + """Convert a string to markdown.""" + converted_md = markdown2.markdown(text.replace("<", "<"), + extras=["spoiler", "tables", "smarty-pants", "fenced-code-blocks"]) + converted_md = re.sub(r"{https?://(?:www\.)?(?:youtube\.com/watch\?.*?&?v=|youtu.be/)([0-9A-Za-z-]+).*?}", + r'
' + r' ' + r'
', converted_md) + converted_md = re.sub(r"{https?://clyp.it/([a-z0-9]+)}", + r'
' + r' ' + r'
', converted_md) + return Markup(converted_md) + + +def require_login(f): + @functools.wraps(f) + def func(*args, **kwargs): + if not fl_g.user: + abort(403) + return + return f(*args, **kwargs) + return func + + @app.errorhandler(400) def error_400(_=None): - return render_template("400.html", g=fl_g) + return render_template("400.html"), 400 @app.route("/400") @@ -41,7 +76,7 @@ def page_400(): @app.errorhandler(403) def error_403(_=None): - return render_template("403.html", g=fl_g) + return render_template("403.html"), 403 @app.route("/403") @@ -51,7 +86,7 @@ def page_403(): @app.errorhandler(500) def error_500(_=None): - return render_template("500.html", g=fl_g) + return render_template("500.html"), 500 @app.route("/500") @@ -61,64 +96,65 @@ def page_500(): @app.route("/") def page_main(): - if not fl_session.get("user_id"): - return redirect(url_for("page_login")) - db_session = db.Session() - royals = db_session.query(db.Royal).order_by(db.Royal.username).all() - wiki_pages = db_session.query(db.WikiEntry).order_by(db.WikiEntry.key).all() - random_diario = db_session.query(db.Diario).order_by(db.func.random()).first() - next_events = db_session.query(db.Event).filter(db.Event.time > datetime.datetime.now()).order_by( + royals = fl_g.session.query(db.Royal).order_by(db.Royal.fiorygi.desc()).all() + wiki_pages = fl_g.session.query(db.WikiEntry).order_by(db.WikiEntry.key).all() + random_diario = fl_g.session.query(db.Diario).order_by(db.func.random()).first() + next_events = fl_g.session.query(db.Event).filter(db.Event.time > datetime.datetime.now()).order_by( db.Event.time).all() - halloween = db.Halloween.puzzle_status()[1] - db_session.close() + quests = fl_g.session.query(db.Quest).all() return render_template("main.html", royals=royals, wiki_pages=wiki_pages, entry=random_diario, - next_events=next_events, g=fl_g, escape=escape, halloween=enumerate(halloween)) + events=next_events, escape=escape, quests=quests) @app.route("/profile/") def page_profile(name: str): - db_session = db.Session() - user = db_session.query(db.Royal).filter_by(username=name).one_or_none() + user = fl_g.session.query(db.Royal).filter_by(username=name).one_or_none() if user is None: - db_session.close() abort(404) return - css = db_session.query(db.ProfileData).filter_by(royal=user).one_or_none() - steam = db_session.query(db.Steam).filter_by(royal=user).one_or_none() - osu = db_session.query(db.Osu).filter_by(royal=user).one_or_none() - dota = db_session.query(db.Dota).join(db.Steam).filter_by(royal=user).one_or_none() - lol = db_session.query(db.LeagueOfLegends).filter_by(royal=user).one_or_none() - ow = db_session.query(db.Overwatch).filter_by(royal=user).one_or_none() - tg = db_session.query(db.Telegram).filter_by(royal=user).one_or_none() - discord = db_session.execute(query_discord_music.one_query, {"royal": user.id}).fetchone() - gamelog = db_session.query(db.GameLog).filter_by(royal=user).one_or_none() - halloween = db_session.query(db.Halloween).filter_by(royal=user).one_or_none() - db_session.close() + css = fl_g.session.query(db.ProfileData).filter_by(royal=user).one_or_none() + mini_data = [] + for game in db.mini_list: + # TODO: investigate on why instrumentedlists are returned, or minis are not found + try: + data = game.mini_get_single_from_royal(fl_g.session, user) + except Exception: + data = None + if data is None: + continue + elif isinstance(data, InstrumentedList): + if len(data) == 0: + continue + mini_data.append({ + "name": game._mini_name, + "data": data[0] + }) + continue + mini_data.append({ + "name": game._mini_name, + "data": data + }) if css is not None: converted_bio = Markup(markdown2.markdown(css.bio.replace("<", "<"), extras=["spoiler", "tables", "smarty-pants", "fenced-code-blocks"])) else: converted_bio = "" - return render_template("profile.html", ryg=user, css=css, osu=osu, dota=dota, lol=lol, steam=steam, ow=ow, - tg=tg, discord=discord, g=fl_g, bio=converted_bio, gamelog=gamelog, - halloween=halloween) + return render_template("profile.html", ryg=user, css=css, bio=converted_bio, mini_data=mini_data) @app.route("/login") def page_login(): - return render_template("login.html", g=fl_g) + return render_template("login.html") @app.route("/loggedin", methods=["POST"]) def page_loggedin(): username = request.form.get("username", "") password = request.form.get("password", "") - db_session = db.Session() - user = db_session.query(db.Royal).filter_by(username=username).one_or_none() - db_session.close() + user = fl_g.session.query(db.Royal).filter_by(username=username).one_or_none() fl_session.permanent = True if user is None: - abort(403) + abort(400) return if user.password is None: fl_session["user_id"] = user.id @@ -128,9 +164,7 @@ def page_loggedin(): fl_session["user_id"] = user.id fl_session["username"] = username return redirect(url_for("page_main")) - else: - abort(403) - return + return redirect(url_for("page_login")) @app.route("/logout") @@ -142,39 +176,27 @@ def page_logout(): @app.route("/password", methods=["GET", "POST"]) +@require_login def page_password(): - if not fl_session.get("user_id"): - return redirect(url_for("page_login")) - user_id = fl_session.get("user_id") if request.method == "GET": - if user_id is None: - return redirect(url_for("page_login")) - return render_template("password.html", g=fl_g) + return render_template("password.html") elif request.method == "POST": new_password = request.form.get("new", "") - db_session = db.Session() - user = db_session.query(db.Royal).filter_by(id=user_id).one() + user = fl_g.session.query(db.Royal).filter_by(id=fl_g.user.id).one() if user.password is None: - user.password = bcrypt.hashpw(bytes(new_password, encoding="utf8"), bcrypt.gensalt()) user.fiorygi += 1 - db_session.commit() - db_session.close() - return redirect(url_for("page_main")) - else: - db_session.close() - return redirect(url_for("page_login")) + user.password = bcrypt.hashpw(bytes(new_password, encoding="utf8"), bcrypt.gensalt()) + fl_g.session.commit() + return redirect(url_for("page_main")) @app.route("/editprofile", methods=["GET", "POST"]) +@require_login def page_editprofile(): - user_id = fl_session.get("user_id") - if not user_id: - return redirect(url_for("page_login")) - db_session = db.Session() - profile_data = db_session.query(db.ProfileData).filter_by(royal_id=user_id).join(db.Royal).one_or_none() + royal = fl_g.session.query(db.Royal).filter_by(id=fl_g.user.id).one() + profile_data = fl_g.session.query(db.ProfileData).filter_by(royal=royal).one_or_none() if request.method == "GET": - db_session.close() - return render_template("profileedit.html", data=profile_data, g=fl_g) + return render_template("profileedit.html", royal=royal, data=profile_data) elif request.method == "POST": css = request.form.get("css", "") bio = request.form.get("bio", "") @@ -182,233 +204,181 @@ def page_editprofile(): abort(400) return if profile_data is None: - profile_data = db.ProfileData(royal_id=user_id, css=css, bio=bio) - db_session.add(profile_data) - db_session.flush() - profile_data.royal.fiorygi += 1 - try: - telegram_bot.send_message(config["Telegram"]["main_group"], - f'⭐️ {profile_data.royal.username} ha' - f' configurato la sua bio' - f' su Royalnet e ha ottenuto un fioryg!', - parse_mode="HTML", disable_web_page_preview=True, disable_notification=True) - except Exception: - pass + profile_data = db.ProfileData(royal=royal, css=css, bio=bio) + fl_g.session.add(profile_data) + fl_g.session.flush() else: profile_data.css = css profile_data.bio = bio - db_session.commit() - royal = db_session.query(db.Royal).filter_by(id=user_id).one() - db_session.close() + fl_g.session.commit() return redirect(url_for("page_profile", name=royal.username)) @app.route("/game/") def page_game(name: str): - db_session = db.Session() - if name == "rl": - game_name = "Rocket League" - query = db_session.query(db.RocketLeague).join(db.Steam).order_by(db.RocketLeague.solo_std_rank).all() - elif name == "dota": - game_name = "Dota 2" - query = db_session.query(db.Dota).join(db.Steam).order_by(db.Dota.rank_tier.desc().nullslast()).all() - elif name == "lol": - game_name = "League of Legends" - query = db_session.query(db.LeagueOfLegends).order_by(db.LeagueOfLegends.solo_division.desc().nullslast(), - db.LeagueOfLegends.solo_rank, - db.LeagueOfLegends.flex_division.desc().nullslast(), - db.LeagueOfLegends.flex_rank, - db.LeagueOfLegends.twtr_division.desc().nullslast(), - db.LeagueOfLegends.twtr_rank, - db.LeagueOfLegends.level).all() - elif name == "osu": - game_name = "osu!" - query = db_session.query(db.Osu).order_by(db.Osu.mania_pp.desc().nullslast()).all() - elif name == "ow": - game_name = "Overwatch" - query = db_session.query(db.Overwatch).order_by(db.Overwatch.rank.desc().nullslast()).all() - elif name == "steam": - game_name = "Steam" - query = db_session.query(db.Steam).order_by(db.Steam.persona_name).all() - elif name == "ryg": - game_name = "Royalnet" - query = db_session.query(db.Royal).order_by(db.Royal.username).all() - elif name == "tg": - game_name = "Telegram" - query = db_session.query(db.Telegram).order_by(db.Telegram.telegram_id).all() - elif name == "discord": - game_name = "Discord" - query = [dict(row) for row in db_session.execute(query_discord_music.all_query)] - elif name == "halloween2018": - game_name = "Rituale di Halloween" - query = db_session.query(db.Halloween).all() + for game in db.mini_list: + if game._mini_name == name: + query = game.mini_get_all(fl_g.session) + break else: abort(404) return - db_session.close() - return render_template("game.html", minis=query, game_name=game_name, game_short_name=name, g=fl_g) + return render_template("game.html", mini_type=game, mini_data=query) -@app.route("/wiki") -def page_wikihome(): - db_session = db.Session() - wiki_pages = db_session.query(db.WikiEntry).order_by(db.WikiEntry.key).all() - db_session.close() - return render_template("wikilist.html", wiki_pages=wiki_pages, g=fl_g) - - -@app.route("/wiki/", methods=["GET", "POST"]) +@app.route("/wiki/") def page_wiki(key: str): - db_session = db.Session() - wiki_page = db_session.query(db.WikiEntry).filter_by(key=key).one_or_none() - if request.method == "GET": - wiki_latest_edit = db_session.query(db.WikiLog).filter_by(edited_key=key) \ - .order_by(db.WikiLog.timestamp.desc()).first() - db_session.close() - if wiki_page is None: - return render_template("wikipage.html", key=key, g=fl_g) - # Embed YouTube videos - converted_md = markdown2.markdown(wiki_page.content.replace("<", "<"), - extras=["spoiler", "tables", "smarty-pants", "fenced-code-blocks"]) - converted_md = re.sub(r"{https?://(?:www\.)?(?:youtube\.com/watch\?.*?&?v=|youtu.be/)([0-9A-Za-z-]+).*?}", - r'
' - r' ' - r'
', converted_md) - converted_md = re.sub(r"{https?://clyp.it/([a-z0-9]+)}", - r'
' - r' ' - r'
', converted_md) - return render_template("wikipage.html", key=key, wiki_page=wiki_page, converted_md=Markup(converted_md), - wiki_log=wiki_latest_edit, g=fl_g) - elif request.method == "POST": - user_id = fl_session.get('user_id') - user = db_session.query(db.Royal).filter_by(id=user_id).one() - if user_id is None: - db_session.close() - return redirect(url_for("page_login")) - new_content = request.form.get("content") - # Create new page - if wiki_page is None: - difference = len(new_content) - wiki_page = db.WikiEntry(key=key, content=new_content) - db_session.add(wiki_page) - db_session.flush() - # Edit existing page + wiki_page = fl_g.session.query(db.WikiEntry).filter_by(key=key).one_or_none() + wiki_latest_edit = fl_g.session.query(db.WikiLog).filter_by(edited_key=key) \ + .order_by(db.WikiLog.timestamp.desc()).first() + if wiki_page is None: + return render_template("wikipage.html", key=key, wiki_page=None) + # Embed YouTube videos + converted_md = markdown2.markdown(wiki_page.content.replace("<", "<"), + extras=["spoiler", "tables", "smarty-pants", "fenced-code-blocks"]) + converted_md = re.sub(r"{https?://(?:www\.)?(?:youtube\.com/watch\?.*?&?v=|youtu.be/)([0-9A-Za-z-]+).*?}", + r'
' + r' ' + r'
', converted_md) + converted_md = re.sub(r"{https?://clyp.it/([a-z0-9]+)}", + r'
' + r' ' + r'
', converted_md) + return render_template("wikipage.html", key=key, wiki_page=wiki_page, converted_md=Markup(converted_md), + wiki_log=wiki_latest_edit) + + +@app.route("/wiki//edit", methods=["POST"]) +@require_login +def page_wiki_edit(key: str): + wiki_page = fl_g.session.query(db.WikiEntry).filter_by(key=key).one_or_none() + if wiki_page is not None and wiki_page.locked: + abort(403) + return + new_content = request.form.get("content") + # Create new page + if wiki_page is None: + difference = len(new_content) + wiki_page = db.WikiEntry(key=key, content=new_content) + fl_g.session.add(wiki_page) + fl_g.session.flush() + # Edit existing page + else: + difference = len(new_content) - len(wiki_page.content) + wiki_page.content = new_content + # Award fiorygi + if difference > 500: + fiorygi = difference // 500 + fiorygi_word = "fioryg" + ("i" if fiorygi != 1 else "") + fl_g.user.fiorygi += fiorygi + else: + fiorygi = 0 + fiorygi_word = "" + edit_reason = request.form.get("reason") + new_log = db.WikiLog(editor=fl_g.user, edited_key=key, timestamp=datetime.datetime.now(), reason=edit_reason) + fl_g.session.add(new_log) + fl_g.session.commit() + try: + reply_msg(telegram_bot, config["Telegram"]["main_group"], strings.WIKI.PAGE_UPDATED, + key=key, + user=fl_g.user.telegram.mention(), + reason=edit_reason, + change=f"+{str(difference)}" if difference > 0 else str(difference)) + if fiorygi > 0: + reply_msg(telegram_bot, config["Telegram"]["main_group"], strings.TELEGRAM.FIORYGI_AWARDED, + mention=fl_g.user.telegram.mention(), + number=fiorygi, + fiorygi=fiorygi_word, + reason="aver contribuito alla wiki Royal Games") + except Exception: + pass + return redirect(url_for("page_wiki", key=key)) + + +@app.route("/wiki//lock", methods=["POST"]) +@require_login +def page_wiki_lock(key: str): + wiki_page = fl_g.session.query(db.WikiEntry).filter_by(key=key).one_or_none() + if wiki_page is None: + abort(404) + return + if fl_g.user.role != "Admin": + abort(403) + return + wiki_page.locked = not wiki_page.locked + try: + if wiki_page.locked: + telegram_bot.send_message(config["Telegram"]["main_group"], + strings.safely_format_string(strings.WIKI.PAGE_LOCKED, + words={ + "key": key, + "user": fl_g.user.username + }), + parse_mode="HTML", + disable_web_page_preview=True, + disable_notification=True) else: - difference = len(new_content) - len(wiki_page.content) - wiki_page.content = new_content - # Award fiorygi - if difference > 50: - fioryg_chance = -(5000/difference) + 100 - fioryg_roll = random.randrange(0, 100) - if fioryg_roll > fioryg_chance: - user.fiorygi += 1 - else: - fioryg_chance = -1 - fioryg_roll = -2 - edit_reason = request.form.get("reason") - new_log = db.WikiLog(editor=user, edited_key=key, timestamp=datetime.datetime.now(), reason=edit_reason) - db_session.add(new_log) - db_session.commit() - message = f'ℹ️ La pagina wiki {key} è stata' \ - f' modificata da' \ - f' {user.username}' \ - f' {"(" + edit_reason + ")" if edit_reason else ""}' \ - f' [{"+" if difference > 0 else ""}{difference}]\n' - if fioryg_roll > fioryg_chance: - message += f"⭐️ {user.username} è stato premiato con 1 fioryg per la modifica!" - try: - telegram_bot.send_message(config["Telegram"]["main_group"], message, - parse_mode="HTML", disable_web_page_preview=True, disable_notification=True) - except Exception: - pass - return redirect(url_for("page_wiki", key=key)) + telegram_bot.send_message(config["Telegram"]["main_group"], + strings.safely_format_string(strings.WIKI.PAGE_UNLOCKED, + words={ + "key": key, + "user": fl_g.user.username + }), + parse_mode="HTML", + disable_web_page_preview=True, + disable_notification=True) + except Exception: + pass + fl_g.session.commit() + return redirect(url_for("page_wiki", key=key)) @app.route("/diario") +@require_login def page_diario(): - user_id = fl_session.get("user_id") - if not user_id: - return redirect(url_for("page_login")) - db_session = db.Session() - diario_entries = db_session.query(db.Diario).order_by(db.Diario.timestamp.desc()).all() - db_session.close() - return render_template("diario.html", g=fl_g, entries=diario_entries) + diario_entries = fl_g.session.query(db.Diario).order_by(db.Diario.timestamp.desc()).all() + return render_template("diario.html", entries=diario_entries) @app.route("/music") def page_music(): - db_session = db.Session() - songs = db_session.execute(query_discord_music.top_songs) - db_session.close() + songs = fl_g.session.execute(sql_queries.top_songs) return render_template("topsongs.html", songs=songs) @app.route("/music/") def page_music_individual(discord_id: str): - db_session = db.Session() - discord = db_session.query(db.Discord).filter_by(discord_id=discord_id).one_or_none() + discord = fl_g.session.query(db.Discord).filter_by(discord_id=discord_id).one_or_none() if discord is None: - db_session.close() abort(404) return - songs = db_session.execute(query_discord_music.single_top_songs, {"discordid": discord.discord_id}) - db_session.close() + songs = fl_g.session.execute(sql_queries.single_top_songs, {"discordid": discord.discord_id}) return render_template("topsongs.html", songs=songs, discord=discord) @app.route("/activity") def page_activity(): - db_session = db.Session() - reports = list(db_session.query(db.ActivityReport).order_by(db.ActivityReport.timestamp.desc()).limit(192).all()) - db_session.close() - return render_template("activity.html", activityreports=list(reversed(reports))) - - -@app.route("/api/token") -def api_token(): - username = request.form.get("username", "") - password = request.form.get("password", "") - db_session = db.Session() - user = db_session.query(db.Royal).filter_by(username=username).one_or_none() - if user is None: - db_session.close() - abort(403) - return - if user.password is None: - db_session.close() - abort(403) - if bcrypt.checkpw(bytes(password, encoding="utf8"), user.password): - new_token = db.LoginToken(royal=user, token=secrets.token_urlsafe()) - db_session.add(new_token) - db_session.commit() - db_session.close() - return jsonify({ - "id": user.id, - "username": user.username, - "token": new_token.token - }) - else: - abort(403) - return - - -@app.route("/ses/identify") -def ses_identify(): - response = jsonify({ - "username": fl_session.get("username"), - "id": fl_session.get("user_id") - }) - response.headers["Access-Control-Allow-Origin"] = "https://steffo.eu" - response.headers["Access-Control-Allow-Credentials"] = "true" - return response + reports = list(fl_g.session.query(db.ActivityReport).order_by(db.ActivityReport.timestamp.desc()).limit(192).all()) + hourly_avg = list(fl_g.session.execute(sql_queries.activity_by_hour, {"current_month": datetime.datetime.now().month})) + previous_month = datetime.datetime.now().month - 1 + if previous_month == 0: + previous_month = 12 + hourly_comp = list(fl_g.session.execute(sql_queries.activity_by_hour, {"current_month": previous_month})) + even_before_month = previous_month - 1 + if even_before_month == 0: + even_before_month = 12 + hourly_before = list(fl_g.session.execute(sql_queries.activity_by_hour, {"current_month": even_before_month})) + return render_template("activity.html", activityreports=list(reversed(reports)), + hourly_avg=hourly_avg, + hourly_comp=hourly_comp, + hourly_before=hourly_before) @app.route("/hooks/github", methods=["POST"]) @@ -422,20 +392,37 @@ def hooks_github(): abort(400) return # TODO: add secret check - message = f"🐙 Nuovi aggiornamenti a Royalnet:\n" - for commit in j.get("commits", []): - if commit["distinct"]: - message += f'{commit["message"]}' \ - f' di {commit["author"].get("username", "anonimo")}\n' - telegram_bot.send_message(config["Telegram"]["main_group"], message, - parse_mode="HTML", disable_web_page_preview=True, disable_notification=True) - return "Done." + if j["ref"] == "refs/heads/master": + message = f"🐙 Nuovi aggiornamenti a Royalnet master:\n" + elif j["ref"] == "refs/heads/unity": + message = f"🐙 Progresso di Royalnet unity:\n" + else: + return "Ignored." + if message: + for commit in j.get("commits", []): + if commit["distinct"]: + message += f'{commit["message"]}' \ + f' di {commit["author"].get("username", "anonimo")}\n' + telegram_bot.send_message(config["Telegram"]["main_group"], message, + parse_mode="HTML", disable_web_page_preview=True, disable_notification=True) + return "Done." @app.before_request def pre_request(): fl_g.css = "nryg.less" fl_g.rygconf = config + fl_g.session = db.Session() + try: + fl_g.user = fl_g.session.query(db.Royal).filter_by(id=fl_session["user_id"]).one_or_none() + except KeyError: + fl_g.user = None + + +@app.after_request +def after_request(response): + fl_g.session.close() + return response if __name__ == "__main__":