PKVXNWpclashogram/__init__.py"""Clash of Clans war moniting for telegram channels.""" __version__ = '0.5.3' from ._clashogram import main __all__ = ['main'] PKFN`0pkkclashogram/_clashogram.py#!/usr/bin/env python """clashogram - Clash of Clans war moniting for telegram channels.""" import os import time import json import shelve import locale import gettext import hashlib import click import jdatetime import pytz import requests from dateutil.parser import parse as dateutil_parse gettext.bindtextdomain('messages', localedir=os.path.join( os.path.dirname(os.path.realpath(__file__)), 'locales')) gettext.textdomain('messages') _ = gettext.gettext POLL_INTERVAL = 60 @click.command() @click.option('--coc-token', help='CoC API token. Reads COC_API_TOKEN env var.', envvar='COC_API_TOKEN', prompt=True) @click.option('--clan-tag', help='Tag of clan without hash. Reads COC_CLAN_TAG env var.', envvar='COC_CLAN_TAG', prompt=True) @click.option('--bot-token', help='Telegram bot token. The bot must be admin on the channel.' 'Reads TELEGRAM_BOT_TOKEN env var.', envvar='TELEGRAM_BOT_TOKEN', prompt=True) @click.option('--channel-name', help='Name of telegram channel for updates.' 'Reads TELEGRAM_CHANNEL env var.', envvar='TELEGRAM_CHANNEL', prompt=True) @click.option('--forever', is_flag=True, help='Try to connect to the CoC server, no matter what.') @click.option('--mute-attacks', is_flag=True, help='Do not send attack updates.') def main(coc_token, clan_tag, bot_token, channel_name, forever, mute_attacks): """Publish war updates to a telegram channel.""" monitor_currentwar(coc_token, clan_tag, bot_token, channel_name, forever, mute_attacks) def monitor_currentwar(coc_token, clan_tag, bot_token, channel_name, forever, mute_attacks): """Send war news to telegram channel.""" with shelve.open('warlog.db', writeback=True) as db: coc_api = CoCAPI(coc_token) notifier = TelegramNotifier(bot_token, channel_name) monitor = WarMonitor(db, coc_api, notifier) while True: try: warinfo = coc_api.get_currentwar(clan_tag) save_latest_data(warinfo.data, monitor) monitor.update(warinfo, mute_attacks=mute_attacks) db.sync() time.sleep(POLL_INTERVAL) except (KeyboardInterrupt, SystemExit): db.sync() db.close() raise except Exception as err: if '500' in str(err) and forever: print('CoC internal server error, retrying.') notifier.send( 'CoC internal server error, retrying in {} seconds.' .format(POLL_INTERVAL * 10)) time.sleep(POLL_INTERVAL * 10) continue if '502' in str(err) and forever: print('CoC bad gateway, retrying.') notifier.send( 'CoC bad gateway, retrying in {} seconds.' .format(POLL_INTERVAL * 10)) time.sleep(POLL_INTERVAL * 10) continue if '503' in str(err): print('CoC maintenance error, retrying.') notifier.send( 'CoC maintenance error, retrying in {} seconds.' .format(POLL_INTERVAL * 10)) time.sleep(POLL_INTERVAL * 10) continue monitor.send( _("☠️ 😵 App is broken boss! Come over and fix me please!")) db.close() raise def save_wardata(wardata): if wardata['state'] != 'notInWar': war_id = "{0}{1}".format(wardata['clan']['tag'][1:], wardata['preparationStartTime']) if not os.path.exists('warlog'): os.mkdir('warlog') path = os.path.join('warlog', war_id) json.dump(wardata, open(path, 'w', encoding='utf-8'), ensure_ascii=False) def save_latest_data(wardata, monitor): if wardata: save_wardata(wardata) json.dump(wardata, open('latest_downloaded_wardata.json', 'w', encoding='utf-8'), ensure_ascii=False) ######################################################################## # Notifiers ######################################################################## class TelegramNotifier(object): def __init__(self, bot_token, channel_name): self.bot_token = bot_token self.channel_name = channel_name def send(self, msg): endpoint = "https://api.telegram.org/bot{bot_token}/sendMessage?"\ "parse_mode={mode}&chat_id=@{channel_name}&text={text}"\ .format(bot_token=self.bot_token, mode='HTML', channel_name=self.channel_name, text=requests.utils.quote(msg)) requests.post(endpoint) ######################################################################## # CoC API Calls ######################################################################## class CoCAPI(object): def __init__(self, coc_token): self.coc_token = coc_token def get_currentwar(self, clan_tag): return WarInfo(self.call_api(self.get_currentwar_endpoint(clan_tag))) def get_claninfo(self, clan_tag): return ClanInfo(self.call_api(self.get_claninfo_endpoint(clan_tag))) def call_api(self, endpoint): s = requests.Session() res = s.get(endpoint, headers={'Authorization': 'Bearer %s' % self.coc_token}) if res.status_code == requests.codes.ok: return json.loads(res.content.decode('utf-8')) else: raise Exception('Error calling CoC API: %s' % res) def get_currentwar_endpoint(self, clan_tag): return 'https://api.clashofclans.com/v1/clans/{clan_tag}/currentwar'\ .format(clan_tag=requests.utils.quote('#%s' % clan_tag)) def get_claninfo_endpoint(self, clan_tag): return 'https://api.clashofclans.com/v1/clans/{clan_tag}'.format( clan_tag=requests.utils.quote(clan_tag)) ######################################################################## # Models according to CoC API ######################################################################## class ClanInfo(object): def __init__(self, clandata): self.data = clandata def get_location(self): return self.data['location']['name'] def get_country_flag_imoji(self): if self.data['location']['isCountry']: return self._get_country_flag_imoji( self.data['location']['countryCode']) elif self.data['location']['name'] == 'International': # The unicode character for planet earth, not empty string! return '🌎' else: return '' def _get_country_flag_imoji(self, country_code): return "{}{}".format(chr(127397 + ord(country_code[0])), chr(127397 + ord(country_code[1]))) def get_winstreak(self): return self.data['warWinStreak'] class WarInfo(object): def __init__(self, wardata): self.data = wardata self.clan_members = {} self.opponent_members = {} self.players = {} self.ordered_attacks = None self._populate() @property def state(self): return self.data['state'] @property def clan_tag(self): return self.data['clan']['tag'] @property def op_tag(self): return self.data['opponent']['tag'] @property def clan_name(self): return self.data['clan']['name'] @property def op_name(self): return self.data['opponent']['name'] @property def clan_level(self): return self.data['clan']['clanLevel'] @property def op_level(self): return self.data['opponent']['clanLevel'] @property def clan_destruction(self): return self.data['clan']['destructionPercentage'] @property def op_destruction(self): return self.data['opponent']['destructionPercentage'] @property def clan_stars(self): return self.data['clan']['stars'] @property def op_stars(self): return self.data['opponent']['stars'] @property def clan_attacks(self): return self.data['clan']['attacks'] @property def op_attacks(self): return self.data['opponent']['attacks'] @property def start_time(self): return self.data['startTime'] @property def team_size(self): return self.data['teamSize'] def _populate(self): if self.is_not_in_war(): return for member in self.data['clan']['members']: self.clan_members[member['tag']] = member self.players[member['tag']] = member for opponent in self.data['opponent']['members']: self.opponent_members[opponent['tag']] = opponent self.players[opponent['tag']] = opponent self.ordered_attacks = self.get_ordered_attacks() def get_ordered_attacks(self): ordered_attacks = {} for player in self.players.values(): for attack in self.get_player_attacks(player): ordered_attacks[attack['order']] = (player, attack) return ordered_attacks def get_player_attacks(self, player): if 'attacks' in player: return player['attacks'] else: return [] def get_player_info(self, tag): if tag not in self.players: raise Exception('Player %s not found.' % tag) return self.players[tag] def is_not_in_war(self): return self.data['state'] == 'notInWar' def is_in_preparation(self): return self.data['state'] == 'preparation' def is_in_war(self): return self.data['state'] == 'inWar' def is_war_over(self): return self.data['state'] == 'warEnded' def is_clan_member(self, player): return player['tag'] in self.clan_members def is_win(self): if self.data['clan']['stars'] > self.data['opponent']['stars']: return True elif self.data['clan']['stars'] == self.data['opponent']['stars'] and\ (self.data['clan']['destructionPercentage'] > self.data['opponent']['destructionPercentage']): return True else: return False def is_draw(self): return \ self.data['clan']['stars'] == self.data['opponent']['stars'] and\ (self.data['clan']['destructionPercentage'] == self.data['opponent']['destructionPercentage']) def create_war_id(self): return "{0}{1}{2}".format(self.data['clan']['tag'], self.data['opponent']['tag'], self.data['preparationStartTime']) ######################################################################## # War statistics ######################################################################## class WarStats(object): def __init__(self, warinfo): self.warinfo = warinfo def calculate_war_stats_sofar(self, attack_order): """Calculate latest war stats. CoC data is updated every 10 minutes and reflects stats after the last attack. We have to calculate the necesssary info for the previous ones""" info = {} info['clan_destruction'] = 0 info['op_destruction'] = 0 info['clan_stars'] = 0 info['op_stars'] = 0 info['clan_used_attacks'] = 0 info['op_used_attacks'] = 0 for order in range(1, attack_order + 1): player, attack = self.warinfo.ordered_attacks[order] if self.warinfo.is_clan_member(player): info['clan_destruction'] +=\ self.get_attack_new_destruction(attack) info['clan_stars'] += self.get_attack_new_stars(attack) info['clan_used_attacks'] += 1 else: info['op_destruction'] +=\ self.get_attack_new_destruction(attack) info['op_stars'] += self.get_attack_new_stars(attack) info['op_used_attacks'] += 1 info['op_destruction'] /= self.warinfo.team_size info['clan_destruction'] /= self.warinfo.team_size return info def get_latest_war_stats(self): return {'clan_destruction': self.warinfo.clan_destruction, 'op_destruction': self.warinfo.op_destruction, 'clan_stars': self.warinfo.clan_stars, 'op_stars': self.warinfo.op_stars, 'clan_used_attacks': self.warinfo.clan_attacks, 'op_used_attacks': self.warinfo.op_attacks,} def get_attack_new_destruction(self, attack): if (attack['destructionPercentage'] > self.get_best_attack_destruction_upto(attack)): return (attack['destructionPercentage'] - self.get_best_attack_destruction_upto(attack)) else: return 0 def get_best_attack_destruction(self, attack): defender = self.warinfo.get_player_info(attack['defenderTag']) if 'bestOpponentAttack' in defender and\ (defender['bestOpponentAttack']['attackerTag'] != attack['attackerTag']): return defender['bestOpponentAttack']['destructionPercentage'] else: return 0 def get_best_attack_destruction_upto(self, in_attack): best_score = 0 for order in range(1, in_attack['order'] + 1): player, attack = self.warinfo.ordered_attacks[order] if attack['defenderTag'] == in_attack['defenderTag'] and\ attack['destructionPercentage'] > best_score and\ attack['attackerTag'] != in_attack['attackerTag']: best_score = attack['destructionPercentage'] return best_score def get_attack_new_stars(self, attack): existing_stars = self.get_best_attack_stars_upto(attack) stars = attack['stars'] - existing_stars if stars > 0: return stars else: return 0 def get_best_attack_stars_upto(self, in_attack): best_score = 0 for order in range(1, in_attack['order'] + 1): player, attack = self.warinfo.ordered_attacks[order] if attack['defenderTag'] == in_attack['defenderTag'] and\ attack['stars'] > best_score and\ attack['attackerTag'] != in_attack['attackerTag']: best_score = attack['stars'] return best_score ######################################################################## # Message formatters ######################################################################## class MessageFactory(object): def __init__(self, coc_api, warinfo): self.coc_api = coc_api self.warinfo = warinfo self.warstats = WarStats(warinfo) def create_preparation_msg(self): msg_template = _("""{top_imoji} {war_size} fold war is ahead!
▫️ Clan {ourclan: <{cwidth}} L {ourlevel: <2} +{clanwinstreak} {clanloc}{clanflag}
▪️ Clan {opponentclan: <{cwidth}} L {theirlevel: <2} +{opwinstreak} {oploc}{opflag}
Game begins at {start}. Have fun! {final_emoji} """) clan_extra_info = self.get_clan_extra_info(self.warinfo.clan_tag) op_extra_info = self.get_clan_extra_info(self.warinfo.op_tag) ourclan = self.warinfo.clan_name opclan = self.warinfo.op_name msg = msg_template.format( top_imoji='\U0001F3C1', ourclan=ourclan, ourlevel=self.warinfo.clan_level, opponentclan=opclan, theirlevel=self.warinfo.op_level, ourtag=self.warinfo.clan_tag, opponenttag=self.warinfo.op_tag, start=self.format_time(self.warinfo.start_time), war_size=self.warinfo.team_size, final_emoji='\U0001F6E1', clanloc=clan_extra_info.get_location(), clanflag=clan_extra_info.get_country_flag_imoji(), oploc=op_extra_info.get_location(), opflag=op_extra_info.get_country_flag_imoji(), clanwinstreak=clan_extra_info.get_winstreak(), opwinstreak=op_extra_info.get_winstreak(), cwidth=max(len(ourclan), len(opclan))) return msg def create_players_msg(self): msg = "⚪️" + _(" Players") msg += "\n▪️" + _("Position, TH, name") sorted_players_by_map_position = sorted( self.warinfo.clan_members.items(), key=lambda x: x[1]['mapPosition']) for player_tag, player_info in sorted_players_by_map_position: line = "▫️{map_position: <2d} {thlevel: <2d} {name}"\ .format(thlevel=player_info['townhallLevel'], map_position=player_info['mapPosition'], name=player_info['name']) msg += "\n" + line return "
" + msg + "
" def create_war_msg(self): return _('War has begun!') def create_clan_full_destruction_msg(self, attacker, attack, war_stats): return _('⚪️ We destroyed them 100% boss!') def create_clan_attack_msg(self, member, attack, war_stats): return self.create_attack_msg(member, attack, war_stats, imoji='\U0001F535') def create_opponent_attack_msg(self, member, attack, war_stats): return self.create_attack_msg(member, attack, war_stats, imoji='\U0001F534') def create_attack_msg(self, member, attack, war_stats, imoji=''): msg_template = _("""
{top_imoji} [{order}] {ourclan} vs {opponentclan}
Attacker: TH {attacker_thlevel: <2} MP {attacker_map_position} {attacker_name}
Defender: TH {defender_thlevel: <2} MP {defender_map_position} {defender_name}
Result: {stars} | {destruction_percentage}%
{war_info}
""") defender = self.warinfo.get_player_info(attack['defenderTag']) msg = msg_template.format( order=attack['order'], top_imoji=imoji, ourclan=self.warinfo.clan_name, opponentclan=self.warinfo.op_name, attacker_name=member['name'], attacker_thlevel=member['townhallLevel'], attacker_map_position=member['mapPosition'], defender_name=defender['name'], defender_thlevel=defender['townhallLevel'], defender_map_position=defender['mapPosition'], stars=self.format_star_msg(attack), destruction_percentage=attack['destructionPercentage'], war_info=self.create_war_info_msg(war_stats), nwidth=max(len(member['name']), len(defender['name']))) return msg def format_star_msg(self, attack): new_stars = self.warstats.get_attack_new_stars(attack) cookies = (attack['stars'] - new_stars) * '🍪' stars = new_stars * '⭐' return cookies + stars def create_war_info_msg(self, war_stats): template = _("""▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ {clan_destruction:.2f}% ▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: <{swidth}} ⚡ {opponent_destruction:.2f}%""") clan_stars = war_stats['clan_stars'] op_stars = war_stats['op_stars'] clan_attack_count = war_stats['clan_used_attacks'] op_attack_count = war_stats['op_used_attacks'] return template.format( total=self.warinfo.team_size * 2, clan_attack_count=clan_attack_count, opponent_attack_count=op_attack_count, clan_stars=clan_stars, clan_destruction=war_stats['clan_destruction'], opponent_stars=op_stars, opponent_destruction=war_stats['op_destruction'], swidth=len(str(max(clan_stars, op_stars))), atkwidth=len(str(max(clan_attack_count, op_attack_count)))) def create_opponent_full_destruction_msg(self, attacker, attack, war_stats): return _('⚫️ They destroyed us 100% boss!') def create_war_over_msg(self): msg_template = _("""
{win_or_lose_title}
Clan {ourclan: <{cwidth}} L {ourlevel: <2}
Clan {opponentclan: <{cwidth}} L {theirlevel: <2}
{war_info}
""") ourclan = self.warinfo.clan_name opclan = self.warinfo.op_name msg = msg_template.format( win_or_lose_title=self.create_win_or_lose_title(), ourclan=ourclan, ourlevel=self.warinfo.clan_level, opponentclan=opclan, theirlevel=self.warinfo.op_level, war_info=self.create_war_info_msg( self.warstats.get_latest_war_stats()), cwidth=max(len(ourclan), len(opclan))) return msg def create_win_or_lose_title(self): if self.warinfo.is_win(): return "{} {}".format('🎉', _('We won!')) elif self.warinfo.is_draw(): return "{} {}".format('🏳', _('It\'s a tie!')) else: return "{} {}".format('💩', _('We lost!')) def format_time(self, timestamp): utc_time = dateutil_parse(timestamp, fuzzy=True) langs = set([locale.getlocale()[0], os.environ.get('LANG'), os.environ.get('LANGUAGE')]) if langs.intersection(['fa_IR', 'fa', 'fa_IR.UTF-8', 'Persian_Iran']): self.patch_jdatetime() tehran_time = utc_time.astimezone(pytz.timezone("Asia/Tehran")) fmt = jdatetime.datetime.fromgregorian( datetime=tehran_time).strftime("%a، %d %b %Y %H:%M:%S") return self.convert_to_persian_numbers(fmt) return utc_time.strftime("%a, %d %b %Y %H:%M:%S") def patch_jdatetime(self): jdatetime.date._is_fa_locale = lambda self: True def convert_to_persian_numbers(self, text): # Supper intelligent and super efficient :) return text.replace('0', '۰')\ .replace('1', '۱')\ .replace('2', '۲')\ .replace('3', '۳')\ .replace('4', '۴')\ .replace('5', '۵')\ .replace('6', '۶')\ .replace('7', '۷')\ .replace('8', '۸')\ .replace('9', '۹') def get_clan_extra_info(self, clan_tag): return self.coc_api.get_claninfo(clan_tag) ######################################################################## # Main war monitor class ######################################################################## class WarMonitor(object): def __init__(self, db, coc_api, notifier): self.db = db self.coc_api = coc_api self.notifier = notifier self.warinfo = None self.msg_factory = None self.warstats = None def update(self, warinfo, mute_attacks=False): if warinfo.is_not_in_war(): if self.warinfo is not None: self.send_war_over_msg() self.reset() return self.populate_warinfo(warinfo) if warinfo.is_in_preparation(): self.send_preparation_msg() elif warinfo.is_in_war(): self.send_war_msg() if not mute_attacks: self.send_attack_msgs() elif warinfo.is_war_over(): if not mute_attacks: self.send_attack_msgs() self.send_war_over_msg() self.reset() else: print("Current war status is uknown. We stay quiet.") def populate_warinfo(self, warinfo): self.warinfo = warinfo self.warstats = WarStats(warinfo) self.msg_factory = MessageFactory(self.coc_api, warinfo) if self.get_war_id() not in self.db: self.db[self.get_war_id()] = {} def get_war_id(self): if not self.warinfo: raise ValueError('Warinfo is empty.') return self.warinfo.create_war_id() def send_preparation_msg(self): self.send_once( self.msg_factory.create_preparation_msg(), msg_id='preparation_msg') self.send_once( self.msg_factory.create_players_msg(), msg_id='players_msg') def send_war_msg(self): self.send_once(self.msg_factory.create_war_msg(), 'war_msg') def send_attack_msgs(self): for order, items in sorted(self.warinfo.ordered_attacks.items()): player, attack = items self.send_single_attack_msg(player, attack) def send_single_attack_msg(self, player, attack): war_stats = self.warstats.calculate_war_stats_sofar(attack['order']) if self.warinfo.is_clan_member(player): self.send_clan_attack_msg(player, attack, war_stats) else: self.send_opponent_attack_msg(player, attack, war_stats) def send_clan_attack_msg(self, attacker, attack, war_stats): self.send_once( self.msg_factory.create_clan_attack_msg( attacker, attack, war_stats), msg_id=self.get_attack_id(attack)) if war_stats['clan_destruction'] == 100: self.send_once( self.msg_factory.create_clan_full_destruction_msg( attacker, attack, war_stats), msg_id='clan_full_destruction') def is_msg_sent(self, msg_id): return self.db[self.get_war_id()].get(msg_id, False) def mark_msg_as_sent(self, msg_id): self.db[self.get_war_id()][msg_id] = True def get_attack_id(self, attack): return "attack{}{}".format(attack['attackerTag'][1:], attack['defenderTag'][1:]) def send_opponent_attack_msg(self, attacker, attack, war_stats): self.send_once(self.msg_factory.create_opponent_attack_msg( attacker, attack, war_stats), msg_id=self.get_attack_id(attack)) if war_stats['op_destruction'] == 100: self.send_once( self.msg_factory.create_opponent_full_destruction_msg( attacker, attack, war_stats), msg_id='op_full_destruction') def send_war_over_msg(self): self.send_once( self.msg_factory.create_war_over_msg(), msg_id='war_over_msg') def reset(self): self.warinfo = None self.warstats = None self.msg_factory = None def send_once(self, msg, msg_id=None): if not msg_id: msg_id = hashlib.md5(msg.encode('utf-8')).hexdigest() if not self.is_msg_sent(msg_id): self.send(msg) self.mark_msg_as_sent(msg_id) def send(self, msg): self.notifier.send(msg) if __name__ == '__main__': main() PKFN clashogram/locales/messages.pot# Translations template for Clashogram. # Copyright (C) 2018 ORGANIZATION # This file is distributed under the same license as the Clashogram project. # FIRST AUTHOR , 2018. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Clashogram 0.6.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2018-11-04 11:45+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.6.0\n" #: clashogram/_clashogram.py:95 msgid "☠️ 😵 App is broken boss! Come over and fix me please!" msgstr "" #: clashogram/_clashogram.py:439 msgid "" "{top_imoji} {war_size} fold war is ahead!\n" "
▫️ Clan {ourclan: <{cwidth}} L {ourlevel: <2} +{clanwinstreak} "
"{clanloc}{clanflag}\n"
"▪️ Clan {opponentclan: <{cwidth}} L {theirlevel: <2} +{opwinstreak} "
"{oploc}{opflag}
\n" "Game begins at {start}.\n" "Have fun! {final_emoji}\n" msgstr "" #: clashogram/_clashogram.py:472 msgid " Players" msgstr "" #: clashogram/_clashogram.py:473 msgid "Position, TH, name" msgstr "" #: clashogram/_clashogram.py:487 msgid "War has begun!" msgstr "" #: clashogram/_clashogram.py:490 msgid "⚪️ We destroyed them 100% boss!" msgstr "" #: clashogram/_clashogram.py:501 msgid "" "
{top_imoji} [{order}] {ourclan} vs {opponentclan}\n"
"Attacker: TH {attacker_thlevel: <2} MP {attacker_map_position} "
"{attacker_name}\n"
"Defender: TH {defender_thlevel: <2} MP {defender_map_position} "
"{defender_name}\n"
"Result: {stars} | {destruction_percentage}%\n"
"{war_info}\n"
"
" msgstr "" #: clashogram/_clashogram.py:532 msgid "" "▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ " "{clan_destruction:.2f}%\n" "▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: " "<{swidth}} ⚡ {opponent_destruction:.2f}%" msgstr "" #: clashogram/_clashogram.py:553 msgid "⚫️ They destroyed us 100% boss!" msgstr "" #: clashogram/_clashogram.py:556 msgid "" "
{win_or_lose_title}\n"
"Clan {ourclan: <{cwidth}} L {ourlevel: <2}\n"
"Clan {opponentclan: <{cwidth}} L {theirlevel: <2}\n"
"{war_info}\n"
"
" msgstr "" #: clashogram/_clashogram.py:577 msgid "We won!" msgstr "" #: clashogram/_clashogram.py:579 msgid "It's a tie!" msgstr "" #: clashogram/_clashogram.py:581 msgid "We lost!" msgstr "" PKFN| | 0clashogram/locales/fa_IR/LC_MESSAGES/messages.mo   <#rax  H _ &U Players
{top_imoji} [{order}] {ourclan} vs {opponentclan}
Attacker: TH {attacker_thlevel: <2} MP {attacker_map_position} {attacker_name}
Defender: TH {defender_thlevel: <2} MP {defender_map_position} {defender_name}
Result: {stars} | {destruction_percentage}%
{war_info}
{win_or_lose_title}
Clan {ourclan: <{cwidth}} L {ourlevel: <2}
Clan {opponentclan: <{cwidth}} L {theirlevel: <2}
{war_info}
It's a tie!Position, TH, nameWar has begun!We lost!We won!{top_imoji} {war_size} fold war is ahead!
▫️ Clan {ourclan: <{cwidth}} L {ourlevel: <2} +{clanwinstreak} {clanloc}{clanflag}
▪️ Clan {opponentclan: <{cwidth}} L {theirlevel: <2} +{opwinstreak} {oploc}{opflag}
Game begins at {start}. Have fun! {final_emoji} ▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ {clan_destruction:.2f}% ▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: <{swidth}} ⚡ {opponent_destruction:.2f}%☠️ 😵 App is broken boss! Come over and fix me please!⚪️ We destroyed them 100% boss!Project-Id-Version: Clashogram 1.0 Report-Msgid-Bugs-To: EMAIL@ADDRESS POT-Creation-Date: 2018-11-04 11:45+0100 PO-Revision-Date: 2018-01-27 16:47+0100 Last-Translator: Language: fa_IR Language-Team: fa_IR Plural-Forms: nplurals=1; plural=0 MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.6.0 لیست همبازی‌ها
{top_imoji} {order} ک {ourclan} و {opponentclan}
مهاجم:  ت {attacker_thlevel: <2} ر {attacker_map_position} {attacker_name}
مدافع:  ت {defender_thlevel: <2} ر {defender_map_position} {defender_name}
نتیجه: {stars} | {destruction_percentage}%
{war_info}
{win_or_lose_title}
کلن {ourclan: <{cwidth}} لول {ourlevel: <2}
کلن {opponentclan: <{cwidth}} لول {theirlevel: <2}
{war_info}
مساوی کردیم!ردیف، ت.ه، نامجنگ قبیله شروع شد!باختیم رئیس!بردیم!{top_imoji} وار {war_size} ‌تائی در راه است!
▫️ کلن {ourclan: <{cwidth}} ل {ourlevel: <2} +{clanwinstreak} {clanloc}{clanflag}
▪️ کلن {opponentclan: <{cwidth}} ل {theirlevel: <2} +{opwinstreak} {oploc}{opflag}
بازی {start} شروع می‌شود. شاد باشید! {final_emoji} ▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ {clan_destruction:.2f}% ▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: <{swidth}} ⚡ {opponent_destruction:.2f}%رئیس من ترکیدم! با آدمتون تماس بگیرید بیاد درستم کنه!⚫️ رئیس فول زدیمشون!PKFNݬ0clashogram/locales/fa_IR/LC_MESSAGES/messages.po# Persian (Iran) translations for Clashogram. # Copyright (C) 2017 ORGANIZATION # This file is distributed under the same license as the Clashogram project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Clashogram 1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2018-11-04 11:45+0100\n" "PO-Revision-Date: 2018-01-27 16:47+0100\n" "Last-Translator: \n" "Language: fa_IR\n" "Language-Team: fa_IR \n" "Plural-Forms: nplurals=1; plural=0\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.6.0\n" #: clashogram/_clashogram.py:95 msgid "☠️ 😵 App is broken boss! Come over and fix me please!" msgstr "رئیس من ترکیدم! با آدمتون تماس بگیرید بیاد درستم کنه!" #: clashogram/_clashogram.py:439 msgid "" "{top_imoji} {war_size} fold war is ahead!\n" "
▫️ Clan {ourclan: <{cwidth}} L {ourlevel: <2} +{clanwinstreak} "
"{clanloc}{clanflag}\n"
"▪️ Clan {opponentclan: <{cwidth}} L {theirlevel: <2} +{opwinstreak} "
"{oploc}{opflag}
\n" "Game begins at {start}.\n" "Have fun! {final_emoji}\n" msgstr "" "{top_imoji} وار {war_size} ‌تائی در راه است!\n" "
▫️ کلن {ourclan: <{cwidth}} ل {ourlevel: <2} +{clanwinstreak} "
"{clanloc}{clanflag}\n"
"▪️ کلن {opponentclan: <{cwidth}} ل {theirlevel: <2} +{opwinstreak} "
"{oploc}{opflag}
\n" "بازی {start} شروع می‌شود.\n" "شاد باشید! {final_emoji}\n" #: clashogram/_clashogram.py:472 msgid " Players" msgstr " لیست همبازی‌ها" #: clashogram/_clashogram.py:473 msgid "Position, TH, name" msgstr "ردیف، ت.ه، نام" #: clashogram/_clashogram.py:487 msgid "War has begun!" msgstr "جنگ قبیله شروع شد!" #: clashogram/_clashogram.py:490 msgid "⚪️ We destroyed them 100% boss!" msgstr "⚫️ رئیس فول زدیمشون!" #: clashogram/_clashogram.py:501 msgid "" "
{top_imoji} [{order}] {ourclan} vs {opponentclan}\n"
"Attacker: TH {attacker_thlevel: <2} MP {attacker_map_position} "
"{attacker_name}\n"
"Defender: TH {defender_thlevel: <2} MP {defender_map_position} "
"{defender_name}\n"
"Result: {stars} | {destruction_percentage}%\n"
"{war_info}\n"
"
" msgstr "" "
{top_imoji} {order} ک {ourclan} و {opponentclan}\n"
"مهاجم:  ت {attacker_thlevel: <2} ر {attacker_map_position} "
"{attacker_name}\n"
"مدافع:  ت {defender_thlevel: <2} ر {defender_map_position} "
"{defender_name}\n"
"نتیجه: {stars} | {destruction_percentage}%\n"
"{war_info}\n"
"
" #: clashogram/_clashogram.py:532 msgid "" "▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ " "{clan_destruction:.2f}%\n" "▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: " "<{swidth}} ⚡ {opponent_destruction:.2f}%" msgstr "" #: clashogram/_clashogram.py:553 #, fuzzy msgid "⚫️ They destroyed us 100% boss!" msgstr "⚫️ رئیس فول خوردیم!" #: clashogram/_clashogram.py:556 msgid "" "
{win_or_lose_title}\n"
"Clan {ourclan: <{cwidth}} L {ourlevel: <2}\n"
"Clan {opponentclan: <{cwidth}} L {theirlevel: <2}\n"
"{war_info}\n"
"
" msgstr "" "
{win_or_lose_title}\n"
"کلن {ourclan: <{cwidth}} لول {ourlevel: <2}\n"
"کلن {opponentclan: <{cwidth}} لول {theirlevel: <2}\n"
"{war_info}\n"
"
" #: clashogram/_clashogram.py:577 msgid "We won!" msgstr "بردیم!" #: clashogram/_clashogram.py:579 msgid "It's a tie!" msgstr "مساوی کردیم!" #: clashogram/_clashogram.py:581 msgid "We lost!" msgstr "باختیم رئیس!" #~ msgid "🎉 We won!" #~ msgstr "بردیم!" #~ msgid "🏳 It's a tie!" #~ msgstr "مساوی کردیم!" #~ msgid "💩 We lost!" #~ msgstr "💩 باختیم رئیس!" #~ msgid "⚪️We destroyed them 100% boss!" #~ msgstr "⚫️رئیس فول زدیمشون!" #~ msgid "⚫️They destroyed us 100% boss!" #~ msgstr "⚫️ رئیس فول خوردیم!" PKFN0B -clashogram/locales/ru/LC_MESSAGES/messages.mo <##,P$Q86 I f   ~ 2 [ :d > Players
{top_imoji} [{order}] {ourclan} vs {opponentclan}
Attacker: TH {attacker_thlevel: <2} MP {attacker_map_position} {attacker_name}
Defender: TH {defender_thlevel: <2} MP {defender_map_position} {defender_name}
Result: {stars} | {destruction_percentage}%
{war_info}
{win_or_lose_title}
Clan {ourclan: <{cwidth}} L {ourlevel: <2}
Clan {opponentclan: <{cwidth}} L {theirlevel: <2}
{war_info}
It's a tie!Position, TH, nameWar has begun!We lost!We won!{top_imoji} {war_size} fold war is ahead!
▫️ Clan {ourclan: <{cwidth}} L {ourlevel: <2} +{clanwinstreak} {clanloc}{clanflag}
▪️ Clan {opponentclan: <{cwidth}} L {theirlevel: <2} +{opwinstreak} {oploc}{opflag}
Game begins at {start}. Have fun! {final_emoji} ▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ {clan_destruction:.2f}% ▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: <{swidth}} ⚡ {opponent_destruction:.2f}%☠️ 😵 App is broken boss! Come over and fix me please!⚪️ We destroyed them 100% boss!⚫️ They destroyed us 100% boss!Project-Id-Version: Clashogram Report-Msgid-Bugs-To: EMAIL@ADDRESS POT-Creation-Date: 2018-11-04 11:45+0100 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language: ru Language-Team: ru Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.6.0 Участники
{top_imoji} [{order}] {ourclan} vs {opponentclan}
Атакующий: ТХ: {attacker_thlevel: <2} ПОЗИЦИЯ: {attacker_map_position} {attacker_name}
Защитник:  ТХ: {defender_thlevel: <2} ПОЗИЦИЯ: {defender_map_position} {defender_name}
Результат: {stars} | {destruction_percentage}%
{war_info}
{win_or_lose_title}
Клан {ourclan: <{cwidth}} Уровень: {ourlevel: <2}
Клан {opponentclan: <{cwidth}} Уровень: {theirlevel: <2}
{war_info}
Это ничья!Позиция, ТХ, имяВойна началась!Мы проиграли!Мы победили!{top_imoji} {war_size} Объявлена война!
▫️ Клан {ourclan: <{cwidth}} Уровень: {ourlevel: <2} Серия: +{clanwinstreak} {clanloc}{clanflag}
▪️ Клан {opponentclan: <{cwidth}} Уровень: {theirlevel: <2} Серия: +{opwinstreak} {oploc}{opflag}
Начало битвы в {start}. Удачных сражений! {final_emoji}▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ {clan_destruction:.2f}% ▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: <{swidth}} ⚡ {opponent_destruction:.2f}%☠️ 😵 Приложение упало, босс! Приди и почини меня!⚪️ Мы уничтожили их на 100%, босс!⚫️ Они уничтожили нас на 100%, босс!PKFN'M[[-clashogram/locales/ru/LC_MESSAGES/messages.po msgid "" msgstr "" "Project-Id-Version: Clashogram\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2018-11-04 11:45+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: ru\n" "Language-Team: ru \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.6.0\n" #: clashogram/_clashogram.py:95 msgid "☠️ 😵 App is broken boss! Come over and fix me please!" msgstr "☠️ 😵 Приложение упало, босс! Приди и почини меня!" #: clashogram/_clashogram.py:439 msgid "" "{top_imoji} {war_size} fold war is ahead!\n" "
▫️ Clan {ourclan: <{cwidth}} L {ourlevel: <2} +{clanwinstreak} "
"{clanloc}{clanflag}\n"
"▪️ Clan {opponentclan: <{cwidth}} L {theirlevel: <2} +{opwinstreak} "
"{oploc}{opflag}
\n" "Game begins at {start}.\n" "Have fun! {final_emoji}\n" msgstr "" "{top_imoji} {war_size} Объявлена война!\n" "
▫️ Клан {ourclan: <{cwidth}} Уровень: {ourlevel: <2} Серия: "
"+{clanwinstreak} {clanloc}{clanflag}\n"
"▪️ Клан {opponentclan: <{cwidth}} Уровень: {theirlevel: <2} Серия: "
"+{opwinstreak} {oploc}{opflag}
\n" "Начало битвы в {start}.\n" "Удачных сражений! {final_emoji}" #: clashogram/_clashogram.py:472 msgid " Players" msgstr " Участники" #: clashogram/_clashogram.py:473 msgid "Position, TH, name" msgstr "Позиция, ТХ, имя" #: clashogram/_clashogram.py:487 msgid "War has begun!" msgstr "Война началась!" #: clashogram/_clashogram.py:490 msgid "⚪️ We destroyed them 100% boss!" msgstr "⚪️ Мы уничтожили их на 100%, босс!" #: clashogram/_clashogram.py:501 msgid "" "
{top_imoji} [{order}] {ourclan} vs {opponentclan}\n"
"Attacker: TH {attacker_thlevel: <2} MP {attacker_map_position} "
"{attacker_name}\n"
"Defender: TH {defender_thlevel: <2} MP {defender_map_position} "
"{defender_name}\n"
"Result: {stars} | {destruction_percentage}%\n"
"{war_info}\n"
"
" msgstr "" "
{top_imoji} [{order}] {ourclan} vs {opponentclan}\n"
"Атакующий: ТХ: {attacker_thlevel: <2} ПОЗИЦИЯ: {attacker_map_position} "
"{attacker_name}\n"
"Защитник:  ТХ: {defender_thlevel: <2} ПОЗИЦИЯ: {defender_map_position} "
"{defender_name}\n"
"Результат: {stars} | {destruction_percentage}%\n"
"{war_info}\n"
"
" #: clashogram/_clashogram.py:532 msgid "" "▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ " "{clan_destruction:.2f}%\n" "▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: " "<{swidth}} ⚡ {opponent_destruction:.2f}%" msgstr "" "▪ {clan_attack_count: >{atkwidth}}/{total} ⭐ {clan_stars: <{swidth}} ⚡ " "{clan_destruction:.2f}%\n" "▪ {opponent_attack_count: >{atkwidth}}/{total} ⭐ {opponent_stars: " "<{swidth}} ⚡ {opponent_destruction:.2f}%" #: clashogram/_clashogram.py:553 msgid "⚫️ They destroyed us 100% boss!" msgstr "⚫️ Они уничтожили нас на 100%, босс!" #: clashogram/_clashogram.py:556 msgid "" "
{win_or_lose_title}\n"
"Clan {ourclan: <{cwidth}} L {ourlevel: <2}\n"
"Clan {opponentclan: <{cwidth}} L {theirlevel: <2}\n"
"{war_info}\n"
"
" msgstr "" "
{win_or_lose_title}\n"
"Клан {ourclan: <{cwidth}} Уровень: {ourlevel: <2}\n"
"Клан {opponentclan: <{cwidth}} Уровень: {theirlevel: <2}\n"
"{war_info}\n"
"
" #: clashogram/_clashogram.py:577 msgid "We won!" msgstr "Мы победили!" #: clashogram/_clashogram.py:579 msgid "It's a tie!" msgstr "Это ничья!" #: clashogram/_clashogram.py:581 msgid "We lost!" msgstr "Мы проиграли!" #~ msgid "⚪️We destroyed them 100% boss!" #~ msgstr "⚪️ Мы уничтожили их на 100%, босс!" #~ msgid "⚫️They destroyed us 100% boss!" #~ msgstr "⚫️ Они уничтожили нас на 100%, босс!" PK!H|'.+clashogram-0.5.3.dist-info/entry_points.txtN+I/N.,()JI,O/J̵E0r3PK9wJh &clashogram-0.5.3.dist-info/LICENSE.txtCopyright 2017 Mehdi Sadeghi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!HPO clashogram-0.5.3.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!H]6> t#clashogram-0.5.3.dist-info/METADATAXn$Ҏ"`MD+%JFQ!9vg@&Sƹ \} ~~gf\R)P!39?9VŠ Y6$|"m%L)[> {LSQ,|Hӏ(x3eU6c]p+Ix4Y&u*\Lpܴ[rF:mr+#b9VCc̀^\:jXúOH(R@B%mM ;)U!MpL~ iB^ufef"FVMPFġl mxJgF'r/lYlR#DwF{z$QfAw g2ѹ;‚YR6wFӯ #81YTK[εK2"<8PƶyOi^(Vwj*ގ]o/H&f!x?T?>!5CT m91Y$Ru26㜷c)y< [TI\ԟg\_L:w'gG Wd.?~* N%81@,,/Ӗ >*'\eJcõoC. _負>v'z5`N$sXRi ec[{"5d. AN-,0`0^v"=RG}p_Bޗs.ѵDHD'FF%aA㏱E93*O< FLLVp{GĦj2E߀l090W9\FAOlV w)hBf! 6D1Hd6꺽^ E.B[2kA(ë+kiXuwiZFS.Q$^îbP%(gLC7z80V #+o\H+Aá)c=ⰌW;"2Ո5&HΎnEeS,W A.]iXе5zt&A A☣y5B{c8ǚ204Q5c 2(xbYKXoOS@YUb߄ʣҢ6w6{3?zI5¥4j,23*F65\'+dΧI*|dHyyJwȾoLsKhj0ՕCNtakR^W2BU St c%`R2w,)A"Wg{mIՉۄ }=w̫*uk” 0xo=?ݿz紇@VLu_ CjAwx=zq:hd ϻ~=#]1=;b}l2v2`ahRAUguvI%\:\x, 2!J]7.,/γA﹧ 'N`촠s]s4(yC~%~]Hk7Q׾ZNa) YW{t O"+|u3I1gA#(9DRZq̹3wAQi`u_OrM$ (9X&|yՆhG%[ka] ݫb7ѵ[@e/yFq)]VTrLƹ#U)0@Ha;Fd^vbH[^cWENN+GElYx-GٸBT\bhndtA3zjvӹtcZ12-mcpCEJjG=E'-"[z!EFeԶu"%d5D]vwKfF>^1H~pc(p\5ױHQWkcNݻ {n`Gc tnD}s?S:M V41~vttDg6Lnu' u^$jaEH{=*ԟLaG?;s5e'u]EvNwO{[$ Ѐ|D6d >jo?UG%1/$|WW h\Jf`zoluU>@=r$x`^"KK.Qm y߯c,_`_ᣍsK7d6tq]$O"Q_Uwk)Z:;DZnFz:FשM?3ɱZߖQD[v7QԋT8֗J׍{Gݾ4|G^y W7BZ 8~\NYgFʹ~Dgrw3gBdѴ t= e5t2"gr wEjU2-3uSLSɓ>iyZ^ϠpܩJ}ov7_IhJ&f[ܩQs8:X>F?RR*Q^a1r(z@IRlxgI2DO˟oowۿ-bwo͖?%Q|Xw^nCZ/WI[:SO_HPK!HZW/!clashogram-0.5.3.dist-info/RECORDIs@{~ v0AT҅l"; 'Srjn}z"@:¬z̈́k@/y:?C)k":qj5 #i-|ҾOek^jR\fԎI0>Q<dQA2F(Hch0{P|. Izivfkq갡^MY E_~FpWY۞kdXqX;zV;hd~"|ov ]7RBH ԰Xkl{p!0~P7k9=aEDQ o&[qѣ130]7j=_JtG-p & q.Zh37$08euW#CK5'|yC? g65GƱv +v Q!VH3뺫h}Z^J t G}iaoPKVXNWpclashogram/__init__.pyPKFN`0pkk큷clashogram/_clashogram.pyPKFN lclashogram/locales/messages.potPKFN| | 0vclashogram/locales/fa_IR/LC_MESSAGES/messages.moPKFNݬ0Kclashogram/locales/fa_IR/LC_MESSAGES/messages.poPKFN0B -Xclashogram/locales/ru/LC_MESSAGES/messages.moPKFN'M[[-clashogram/locales/ru/LC_MESSAGES/messages.poPK!H|'.+'clashogram-0.5.3.dist-info/entry_points.txtPK9wJh &clashogram-0.5.3.dist-info/LICENSE.txtPK!HPO clashogram-0.5.3.dist-info/WHEELPK!H]6> t#clashogram-0.5.3.dist-info/METADATAPK!HZW/!clashogram-0.5.3.dist-info/RECORDPK