Bot container restart from admin-panel #1

Merged
trevor merged 1 commits from dev into master 2025-07-21 08:20:19 +00:00
267 changed files with 2307 additions and 300 deletions

2
lottery/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .lottery.celery import app as celery_app
__all__ = ("celery_app",)

View File

@@ -1,37 +1,101 @@
# import requests
# import json
# from dotenv import load_dotenv
# import os
# load_dotenv()
# API_URL='http://106.245.250.243:8000/api/clients'
# API_KEY='g1XqG9lir4RneLuX01VAob+F0MpVCZTpG2is8UBWLZ0='
# def fetch_clients(api_url, api_key):
# """
# Подключаемся к API с использованием заданного X-API-Key
# """
# headers = {
# "X-API-Key": api_key
# }
# try:
# response = requests.get(api_url, headers=headers)
# response.raise_for_status()
# return response.json()
# except requests.exceptions.RequestException as e:
# print(f"Ошибка при запросе к API: {e}")
# return None
# if __name__ == '__main__':
# api_endpoint = f"{os.getenv('API_URL')}"
# api_key = os.getenv("API_KEY")
# print(api_endpoint, api_key)
# if not api_endpoint or not api_key:
# print("Необходимо задать API_URL и API_KEY в .env файле.")
# exit(1)
# clients = fetch_clients(api_endpoint, api_key)
# if clients is not None:
# print(json.dumps(clients, indent=4, ensure_ascii=False))
# else:
# print("Не удалось получить данные с API.")
import requests import requests
import json import json
from dotenv import load_dotenv
import os import os
from dotenv import load_dotenv
load_dotenv('.env') load_dotenv()
def fetch_clients(api_url, api_key): class ApiClient:
""" def __init__(self, base_url=None, api_key=None):
Подключаемся к API с использованием заданного X-API-Key self.base_url = base_url or os.getenv('API_URL', 'http://106.245.250.243:8000/api')
""" self.api_key = api_key or os.getenv('API_KEY')
headers = { if not self.api_key:
"X-API-Key": api_key raise ValueError("API_KEY не задан ни в .env, ни через параметры конструктора")
} self.headers = {
try: "X-API-Key": self.api_key
response = requests.get(api_url, headers=headers) }
response.raise_for_status()
return response.json() def fetch_clients(self):
except requests.exceptions.RequestException as e: """Получение списка клиентов"""
print(f"Ошибка при запросе к API: {e}") try:
return None url = f"{self.base_url}/clients"
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Ошибка при получении клиентов: {e}")
return None
def fetch_invoices(self, filters=None):
"""Получение списка счетов с возможными фильтрами"""
try:
url = f"{self.base_url}/invoices"
response = requests.get(url, headers=self.headers, params=filters)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Ошибка при получении счетов: {e}")
return None
if __name__ == '__main__': if __name__ == '__main__':
api_endpoint = os.getenv("API_URL") client = ApiClient()
api_key = os.getenv("API_KEY")
print("=== Клиенты ===")
print(api_endpoint, api_key) clients = client.fetch_clients()
if not api_endpoint or not api_key: if clients:
print("Необходимо задать API_URL и API_KEY в .env файле.")
exit(1)
clients = fetch_clients(api_endpoint, api_key)
if clients is not None:
print(json.dumps(clients, indent=4, ensure_ascii=False)) print(json.dumps(clients, indent=4, ensure_ascii=False))
else: else:
print("Не удалось получить данные с API.") print("Не удалось получить клиентов")
print("\n=== Счета ===")
filters = {
"page": 1,
# "created_at[after]": "2024-12-01T00:00:00",
# "created_at[before]": "2025-01-01T00:00:00"
}
invoices = client.fetch_invoices(filters)
if invoices:
print(json.dumps(invoices, indent=4, ensure_ascii=False))
else:
print("Не удалось получить счета")

View File

@@ -1,11 +1,36 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path
from django.shortcuts import redirect
from django.contrib import messages
from .models import BotConfig
from .models import BotConfig, WelcomeMessage from .models import BotConfig, WelcomeMessage
from .tasks import restart_bot_container
@admin.register(BotConfig) @admin.register(BotConfig)
class BotConfigAdmin(admin.ModelAdmin): class BotConfigAdmin(admin.ModelAdmin):
list_display = ("bot_name", "channel_id", "bot_token") change_form_template = "admin/botconfig_change_form.html"
search_fields = ("bot_name", "channel_id", "bot_token")
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"<int:botconfig_id>/restart/",
self.admin_site.admin_view(self.restart_bot),
name="botconfig-restart"
),
]
return custom_urls + urls
def restart_bot(self, request, botconfig_id):
try:
BotConfig.objects.get(id=botconfig_id)
except BotConfig.DoesNotExist:
self.message_user(request, "Настройка бота с таким ID не найдена.", level=messages.ERROR)
return redirect("..")
restart_bot_container.delay()
self.message_user(request, "Бот будет перезапущен в фоновом режиме.", messages.SUCCESS)
return redirect("..")
@admin.register(WelcomeMessage) @admin.register(WelcomeMessage)
class WWelcomeMessageAdmin(admin.ModelAdmin): class WWelcomeMessageAdmin(admin.ModelAdmin):

View File

@@ -1,11 +1,12 @@
# bot/handlers.py
import re import re
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from webapp.models import BindingRequest, Client, Invoice from webapp.models import BindingRequest, Client, Invoice
from draw.models import Lottery, DrawResult, LotteryParticipant from draw.models import DrawResult
from bot.state import BINDING_PENDING from bot.state import BINDING_PENDING
from bot.models import BotConfig
@sync_to_async @sync_to_async
def get_client_by_telegram_id(telegram_id: int): def get_client_by_telegram_id(telegram_id: int):
@@ -32,9 +33,11 @@ def create_binding_request(telegram_chat_id, client_card):
client_card=client_card client_card=client_card
) )
async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE): async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message: if not update.message:
return return
chat_id = update.message.chat_id chat_id = update.message.chat_id
client = await get_client_by_telegram_id(chat_id) client = await get_client_by_telegram_id(chat_id)
if not client: if not client:
@@ -45,7 +48,7 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
invoices = await get_invoices_for_client(client) invoices = await get_invoices_for_client(client)
draws = await get_draws_for_client(client) draws = await get_draws_for_client(client)
message_text = f"👤 *Профиль клиента:*\n" \ message_text = f"👤 *Профиль клиента:*\n" \
f"• *Имя:* {client.name}\n" \ f"• *Имя:* {client.name}\n" \
f"• *Клубная карта:* {client.club_card_number}\n\n" f"• *Клубная карта:* {client.club_card_number}\n\n"
@@ -55,8 +58,7 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
show_all = len(invoices) > 5 show_all = len(invoices) > 5
displayed_invoices = invoices[:5] if show_all else invoices displayed_invoices = invoices[:5] if show_all else invoices
for inv in displayed_invoices: for inv in displayed_invoices:
invoice_str = str(inv).split('/')[0].strip() message_text += f" • ID {inv.id} (*{inv.sum}*)\n"
message_text += f"{invoice_str} (*{inv.sum}*)\n"
else: else:
message_text += " _Нет счетов_\n" message_text += " _Нет счетов_\n"
@@ -64,12 +66,8 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
if draws: if draws:
for draw in draws: for draw in draws:
lottery_name = draw.lottery.name if draw.lottery else "неизвестно" lottery_name = draw.lottery.name if draw.lottery else "неизвестно"
invoice_info = (draw.participant.invoice invoice_info = getattr(draw.participant.invoice, 'id', 'неизвестно') if draw.participant else 'неизвестно'
if draw.participant and hasattr(draw.participant, 'invoice') prize_info = getattr(draw.prize, 'reward', 'неизвестно')
else "неизвестно")
prize_info = (draw.prize.reward
if draw.prize and hasattr(draw.prize, 'reward')
else "неизвестно")
message_text += f" • Лотерея: *{lottery_name}*\n Счет: _{invoice_info}_\n Выигрыш: *{prize_info}*\n\n" message_text += f" • Лотерея: *{lottery_name}*\n Счет: _{invoice_info}_\n Выигрыш: *{prize_info}*\n\n"
else: else:
message_text += " _Нет результатов розыгрышей_\n" message_text += " _Нет результатов розыгрышей_\n"
@@ -82,43 +80,37 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(message_text, parse_mode="Markdown", reply_markup=reply_markup) await update.message.reply_text(message_text, parse_mode="Markdown", reply_markup=reply_markup)
async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обработчик команды /bind.
После ввода команды бот запрашивает ввод номера КК и ожидает сообщение с кодом.
Здесь мы просто добавляем ID пользователя в pending.
"""
if not update.message: if not update.message:
return return
telegram_chat_id = update.message.chat_id telegram_chat_id = update.message.chat_id
# Если пользователь уже привязан, сообщаем об этом.
client = await get_client_by_telegram_id(telegram_chat_id) client = await get_client_by_telegram_id(telegram_chat_id)
if client: if client:
await update.message.reply_text("✅ Вы уже привязаны!") await update.message.reply_text("✅ Вы уже привязаны!")
return return
# Добавляем пользователя в список pending и просим ввести номер КК.
BINDING_PENDING.add(telegram_chat_id) BINDING_PENDING.add(telegram_chat_id)
await update.message.reply_text("Введите номер вашей клубной карты (КК):") await update.message.reply_text("Введите номер вашей клубной карты (КК):")
async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE): async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обрабатывает ввод номера КК от пользователя, который находится в состоянии ожидания.
"""
if not update.message or not update.message.text: if not update.message or not update.message.text:
return return
client_card = update.message.text.strip() client_card = update.message.text.strip()
telegram_chat_id = update.message.chat_id telegram_chat_id = update.message.chat_id
if not re.fullmatch(r'\d{3,}', client_card): if not re.fullmatch(r'\d{3,}', client_card):
await update.message.reply_text("❌ Неверный формат клиентской карты. Пожалуйста, введите корректный код (минимум 3 цифры).") await update.message.reply_text("❌ Неверный формат клиентской карты. Пожалуйста, введите минимум 3 цифры.")
return return
await create_binding_request(telegram_chat_id, client_card) await create_binding_request(telegram_chat_id, client_card)
if telegram_chat_id in BINDING_PENDING: BINDING_PENDING.discard(telegram_chat_id)
BINDING_PENDING.remove(telegram_chat_id)
await update.message.reply_text("✅ Заявка отправлена. После подтверждения вы сможете участвовать в чате.") await update.message.reply_text("✅ Заявка отправлена. После подтверждения вы сможете участвовать в чате.")
async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
@@ -132,9 +124,18 @@ async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAU
message_text = "💳 *Все счета:*\n" message_text = "💳 *Все счета:*\n"
if invoices: if invoices:
for inv in invoices: for inv in invoices:
invoice_str = str(inv).split('/')[0].strip() message_text += f" • ID {inv.id} (*{inv.sum}*)\n"
message_text += f"{invoice_str} (*{inv.sum}*)\n"
else: else:
message_text += " _Нет счетов_\n" message_text += " _Нет счетов_\n"
await query.edit_message_text(message_text, parse_mode="Markdown") await query.edit_message_text(message_text, parse_mode="Markdown")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message:
return
bot_config = await sync_to_async(BotConfig.objects.filter(is_active=True).first)()
if bot_config and bot_config.active_welcome:
await update.message.reply_text(bot_config.active_welcome.welcome_message)
else:
await update.message.reply_text("Привет! Бот работает.")

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.6 on 2025-07-20 09:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0005_alter_welcomemessage_welcome_image'),
]
operations = [
migrations.AlterField(
model_name='botconfig',
name='global_block_message',
field=models.TextField(blank=True, help_text='Сообщение, которое показывается пользователям, если чат отключён.', null=True, verbose_name='Сообщение о глобальном блоке'),
),
migrations.AlterField(
model_name='welcomemessage',
name='welcome_image',
field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='static/upload_image/', verbose_name='Приветственное изображение'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.6 on 2025-07-20 23:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0006_alter_botconfig_global_block_message_and_more'),
]
operations = [
migrations.AddField(
model_name='botconfig',
name='active_welcome',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='used_by', to='bot.welcomemessage', verbose_name='Активное приветствие'),
),
migrations.AddField(
model_name='botconfig',
name='is_active',
field=models.BooleanField(default=False, verbose_name='Включен'),
),
migrations.AddField(
model_name='botconfig',
name='welcome_messages',
field=models.ManyToManyField(blank=True, to='bot.welcomemessage', verbose_name='Варианты приветствия'),
),
]

View File

@@ -1,5 +1,6 @@
from django.db import models from django.db import models
class BotConfig(models.Model): class BotConfig(models.Model):
bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API") bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API")
channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения") channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения")
@@ -15,67 +16,79 @@ class BotConfig(models.Model):
verbose_name="Сообщение о глобальном блоке", verbose_name="Сообщение о глобальном блоке",
help_text="Сообщение, которое показывается пользователям, если чат отключён." help_text="Сообщение, которое показывается пользователям, если чат отключён."
) )
welcome_messages = models.ManyToManyField('WelcomeMessage', blank=True, verbose_name="Варианты приветствия")
active_welcome = models.ForeignKey(
'WelcomeMessage',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="used_by",
verbose_name="Активное приветствие"
)
is_active = models.BooleanField(default=False, verbose_name="Включен")
def __str__(self): def __str__(self):
return self.bot_name return self.bot_name
class Meta: class Meta:
verbose_name = "Настройка бота" verbose_name = "Настройка бота"
verbose_name_plural = "Настройки Ботов" verbose_name_plural = "Настройки Ботов"
class WelcomeMessage(models.Model): class WelcomeMessage(models.Model):
bot = models.ForeignKey(BotConfig, on_delete=models.CASCADE, verbose_name="Бот") bot = models.ForeignKey(BotConfig, on_delete=models.CASCADE, verbose_name="Бот")
welcome_message = models.TextField( welcome_message = models.TextField(
verbose_name="Приветственное сообщение", verbose_name="Приветственное сообщение",
blank=True, blank=True,
null=True, null=True,
help_text="Текст, который будет отправлен при запуске команды /start" help_text="Текст, который будет отправлен при запуске команды /start"
) )
welcome_image = models.ImageField( welcome_image = models.ImageField(
upload_to='static/upload_image/', upload_to='static/upload_image/',
verbose_name="Приветственное изображение", verbose_name="Приветственное изображение",
blank=True, blank=True,
null=True, null=True,
help_text="Загрузите изображение для приветствия" help_text="Загрузите изображение для приветствия"
) )
admin_contact = models.CharField( admin_contact = models.CharField(
max_length=255, max_length=255,
verbose_name="Контакт администратора", verbose_name="Контакт администратора",
blank=True, blank=True,
null=True, null=True,
help_text="Ссылка или контакт для связи с администратором" help_text="Ссылка или контакт для связи с администратором"
) )
channel_link = models.URLField( channel_link = models.URLField(
verbose_name="Ссылка на канал", verbose_name="Ссылка на канал",
blank=True, blank=True,
null=True, null=True,
help_text="URL канала бота" help_text="URL канала бота"
) )
group_link = models.URLField( group_link = models.URLField(
verbose_name="Ссылка на группу", verbose_name="Ссылка на группу",
blank=True, blank=True,
null=True, null=True,
help_text="URL группы бота" help_text="URL группы бота"
) )
custom_link1_name = models.CharField( custom_link1_name = models.CharField(
max_length=100, max_length=100,
verbose_name="Название кастомной ссылки 1", verbose_name="Название кастомной ссылки 1",
blank=True, blank=True,
null=True null=True
) )
custom_link1_url = models.URLField( custom_link1_url = models.URLField(
verbose_name="URL кастомной ссылки 1", verbose_name="URL кастомной ссылки 1",
blank=True, blank=True,
null=True null=True
) )
custom_link2_name = models.CharField( custom_link2_name = models.CharField(
max_length=100, max_length=100,
verbose_name="Название кастомной ссылки 2", verbose_name="Название кастомной ссылки 2",
blank=True, blank=True,
null=True null=True
) )
custom_link2_url = models.URLField( custom_link2_url = models.URLField(
verbose_name="URL кастомной ссылки 2", verbose_name="URL кастомной ссылки 2",
blank=True, blank=True,
null=True null=True
) )
@@ -84,4 +97,4 @@ class WelcomeMessage(models.Model):
verbose_name_plural = "Приветственные сообщения" verbose_name_plural = "Приветственные сообщения"
def __str__(self): def __str__(self):
return f"Приветствие для {self.bot}" return f"Приветствие для {self.bot}"

21
lottery/bot/tasks.py Normal file
View File

@@ -0,0 +1,21 @@
from celery import shared_task
import subprocess
@shared_task
def restart_bot_container(container_name="bot"):
try:
result = subprocess.run(
["docker", "restart", container_name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
return f"Success: {result.stdout.strip()}"
except subprocess.CalledProcessError as e:
return f"Error restarting container: {e.stderr.strip()}"
@shared_task
def ping():
return "pong"

View File

@@ -0,0 +1,6 @@
[mariadb-client]
port=3306
socket=/run/mysqld/mysqld.sock
user=healthcheck
password=9hOHOe^WXq`lsXwRba/@]gB3"|[mOFe{

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,269 @@
59,5
59,4
59,3
59,2
59,1
59,0
56,4
56,3
56,2
56,1
56,0
55,4
55,3
55,2
55,1
55,0
53,4
53,3
53,2
53,1
53,0
52,5
52,4
52,3
52,2
52,1
52,0
51,5
51,4
51,3
51,2
51,1
51,0
50,6
50,5
50,4
50,3
50,2
50,1
50,0
37,3
37,2
37,1
37,0
36,4
36,3
36,2
36,1
36,0
35,4
35,3
35,2
35,1
35,0
34,3
34,2
34,1
34,0
32,4
32,3
32,2
32,1
32,0
28,4
28,3
28,2
28,1
28,0
27,4
27,3
27,2
27,1
27,0
26,5
26,4
26,3
26,2
26,1
26,0
25,5
25,4
25,3
25,2
25,1
25,0
22,5
22,4
22,3
22,2
22,1
22,0
20,5
20,4
20,3
20,2
20,1
20,0
18,5
18,4
18,3
18,2
18,1
18,0
11,4
11,3
11,2
11,1
11,0
8,3
8,2
8,1
8,0
7,3
5,5
5,4
5,3
5,2
5,0
4,3
3,2
2,2
1,2
0,9
0,2
1,45
3,44
2,44
1,44
3,43
2,43
1,43
3,42
2,42
1,42
3,41
2,41
1,41
3,40
2,40
1,40
3,39
2,39
1,39
3,38
2,38
1,38
3,37
2,37
1,37
3,36
2,36
1,36
3,35
2,35
1,35
3,34
2,34
1,34
3,33
2,33
1,33
3,32
2,32
1,32
3,31
2,31
1,31
3,30
2,30
1,30
3,29
2,29
1,29
3,28
2,28
1,28
3,27
2,27
1,27
3,26
2,26
1,26
3,25
2,25
1,25
3,24
2,24
1,24
3,23
2,23
1,23
3,22
2,22
1,22
3,21
2,21
1,21
3,20
2,20
1,20
3,19
2,19
1,19
3,18
2,18
1,18
3,17
2,17
1,17
3,16
2,16
1,16
3,15
2,15
1,15
3,14
2,14
1,14
3,13
2,13
1,13
3,12
2,12
1,12
3,11
2,11
1,11
3,10
2,10
1,10
3,9
2,9
1,9
3,8
2,8
1,8
3,7
2,7
1,7
3,6
2,6
1,6
3,5
2,5
1,5
3,4
2,4
1,4
3,3
3,0
2,3
2,0
1,3
1,0
0,6
0,0
0,47
0,46
0,49
0,48
0,45
0,12
0,10
0,8
0,11
0,5
0,7
0,4
0,3

BIN
lottery/db_data/ib_logfile0 Normal file

Binary file not shown.

BIN
lottery/db_data/ibdata1 Normal file

Binary file not shown.

BIN
lottery/db_data/ibtmp1 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
default-character-set=utf8mb4
default-collation=utf8mb4_uca1400_ai_ci

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
11.6.2-MariaDB

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
default-character-set=utf8mb4
default-collation=utf8mb4_uca1400_ai_ci

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More