class ResponseType(enum.Enum):
    NO_READ = 0
    IGNORE = 1
    NO_RESPONSE = 2
    TEXT = 3
    STICKER = 4 class BotResponse:
    def __init__(self, message, type, data=None, onsend_actions=()):
        self.type = type
        self.user_id = message.user_id
        self.peer_id = message.peer_id
        self.message_body = message.body
        self.message_id = message.id
        self.message_has_action = bool(message.action)
        self.data = data
        self.onsend_actions = list(onsend_actions)

    def fake_message(self):
        if self.peer_id == self.user_id:
            return {'user_id': self.user_id}
        else:
            return {'user_id': self.user_id, 'chat_id': self.peer_id - CONF_START}

    @property
    def is_chat(self):
        return self.peer_id > CONF_START

    @property
    def text(self):
        return self.data if self.type == ResponseType.TEXT else ''

    def get_peer_info(self):
        if self.peer_id == self.user_id:
            return PeerInfo(self.user_id)
        return PeerInfo(self.user_id, self.peer_id - CONF_START) class UserCache(Cache):
    def __init__(self, api, fields, invalidate_interval=0):
        super().__init__(api, invalidate_interval)
        self.fields = fields
        if ',' in fields and 'id' in fields.split(','):
            l = fields.split(',')
            l.remove('id')
            self.fields = ','.join(l)

    def _load(self, ids):
        return self.api.users.get(user_ids=','.join(map(str, ids)), fields=self.fields) class ConfCache(Cache):
    def __init__(self, api, self_id=0, invalidate_interval=0):
        super().__init__(api, invalidate_interval)
        self.self_id = self_id

    def _load(self, ids):
        r = self.api.messages.getChat(chat_ids=','.join(map(str, ids)), fields='id') or []
        for conf in r:
            for user in conf['users']:
                if user['id'] == self.self_id:
                    conf['invited_by'] = user['invited_by']
                    break
            else:
                conf['invited_by'] = 0
            del conf['users']
        return r class MessageCache:
    def __init__(self):
        self.user_msg = {}
        self.sender_msg = {}

    def add(self, sender, body, user_id, mid, reply):
        entry = {'id': mid, 'text': body, 'reply': reply, 'count': 1, 'time': time.time(), 'user_id': user_id}
        self.user_msg[user_id] = entry
        self.sender_msg[sender] = entry
        return entry

    def byUser(self, uid):
        return self.user_msg.setdefault(uid, {})

    def bySender(self, pid):
        return self.sender_msg.setdefault(pid, {})

    def updateTime(self, sender, newtime=None):
        if newtime is None:
            newtime = time.time()
        self.sender_msg.setdefault(sender, {})['time'] = newtime

    def dump(self):
        messages = {id(i): i.copy() for i in self.user_msg.values()}
        messages.update({id(i): i.copy() for i in self.sender_msg.values()})
        um = {i: id(self.user_msg[i]) for i in self.user_msg}
        sm = {i: id(self.sender_msg[i]) for i in self.sender_msg}
        return {'messages': messages, 'user': um, 'sender': sm}

    def load(self, data):
        self.user_msg = {int(i): data['messages'][str(j)] for i, j in data['user'].items()}
        self.sender_msg = {int(i): data['messages'][str(j)] for i, j in data['sender'].items()} class FriendController:
    def __init__(self, params, allowed_names_filename):
        self.allowed_filename = allowed_names_filename
        line1, line2 = open(allowed_names_filename, encoding='utf-8').readlines()
        self.allowed = set(line1 + ' ')
        self.banned_substrings = line2.split()
        self.params = params
        self.params.update({'ignored': ''})
        self.fields = self.requiredFields(params)

    @staticmethod
    def requiredFields(params):
        fields = sum([getattr(Checks, i).fields for i in params], [])
        return ','.join(fields) or 'id'

    def isGood(self, fr, need_reason=False):
        reasons = []
        nodel = storage.contains('nodel', fr['id'])
        for fname in self.params:
            if nodel and fname != 'ignored':
                continue
            fun = getattr(Checks, fname)
            if not fun(self, fr, self.params[fname].split()):
                if need_reason:
                    reasons.append(fun.desc.lower() if reasons else fun.desc)
                else:
                    return False
        if need_reason:
            return ', '.join(reasons) or None
        else:
            return True

    def isIgnored(self, uid):
        return storage.contains('ignored', uid) class ThreadManager:  # not thread-safe, should be used only from main thread
    def __init__(self):
        self.timelines = {}
        self.loop = asyncio.new_event_loop()
        self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
        self.loop_thread.start()

    def run(self, key, timeline):
        if self.isBusy(key):
            self.terminate(key)
        self.timelines[key] = timeline
        timeline.future = asyncio.run_coroutine_threadsafe(timeline.run(), self.loop)

    def isBusy(self, key):
        return key in self.timelines and not self.timelines[key].future.done()

    def terminate(self, key):
        if key not in self.timelines:
            return False
        self.timelines[key].terminated = True
        return True

    def gc(self):
        to_del = []
        for i in self.timelines:
            if self.timelines[i].future.done():
                to_del.append(i)
        for i in to_del:
            del self.timelines[i]

    def shutdown(self, timeout):
        started = time.time()
        while time.time() - started < timeout:
            for task in asyncio.Task.all_tasks(self.loop):
                if not task.done():
                    break
            else:
                return True
            time.sleep(5)
        return False

    def get(self, key):
        return self.timelines.get(key) class Timeline:
    def __init__(self, duration=0):
        self.events = []
        self.attr = {}
        self.duration = duration
        self.endtime = 0
        self.terminated = False
        self.is_alive = True

    def do(self, func, need_attr=False):
        async def _f():
            if need_attr:
                func(self.attr)
            else:
                func()
        self.events.append(_f)
        return self

    def sleep(self, seconds):
        async def _f():
            await asyncio.sleep(seconds)
        self.events.append(_f)
        return self

    def sleepUntil(self, seconds, min_sleep=0):
        async def _f():
            rem = max(self.endtime - time.time() - seconds, min_sleep)
            if rem > 0:
                await asyncio.sleep(rem)
        self.events.append(_f)
        return self

    def doEvery(self, interval, func, end_func, do_at_start=True, need_attr=False):
        if do_at_start:
            self.do(func, need_attr)

        async def _f():
            end = end_func()
            while time.time() + interval < end:
                await asyncio.sleep(interval)
                if self.terminated:
                    return
                if need_attr:
                    func(self.attr)
                else:
                    func()
            await asyncio.sleep(max(0, end - time.time()))

        self.events.append(_f)
        return self

    def doEveryUntil(self, interval, func, seconds=0, do_at_start=True, need_attr=False):
        return self.doEvery(interval, func, lambda: self.endtime - seconds, do_at_start, need_attr)

    def doEveryFor(self, interval, func, seconds, do_at_start=True, need_attr=False):
        return self.doEvery(interval, func, lambda: time.time() + seconds, do_at_start, need_attr)

    async def run(self):
        self.endtime = time.time() + self.duration
        for coro in self.events:
            if self.terminated:
                return
            await coro() class MessageReceiver:
    def __init__(self, api, get_dialogs_interval=-1, message_class=IncomingMessage):
        self.api = api
        self.get_dialogs_interval = get_dialogs_interval
        self.longpoll_queue = queue.Queue()
        self.longpoll_thread = threading.Thread(target=self.monitor, daemon=True)
        self.longpoll_thread.start()
        self.longpoll_callback = None
        self.last_message_id = 0
        self.last_get_dialogs = 0
        self.longpolled_messages = set()
        self.terminate_monitor = False
        self.message_class = message_class


    def monitor(self):
        while True:
            try:
                for i in self._getLongpoll():
                    self.longpoll_queue.put(i)
            except Exception:
                logger.exception('MessageReceiver error')
                time.sleep(5)

    def getMessages(self, get_dialogs=False):
        ctime = time.time()
        if not self.last_get_dialogs:
            self.last_get_dialogs = ctime - self.get_dialogs_interval + 1
        if (self.get_dialogs_interval >=0 and ctime - self.last_get_dialogs > self.get_dialogs_interval) or get_dialogs:
            self.last_get_dialogs = ctime
            res = []
            messages = self.api.messages.getConversations(count=200, filter='unread')
            try:
                messages = messages['items'][::-1]
            except TypeError:
                logger.warning('Unable to fetch messages')
                return []
            for msg in sorted(messages, key=lambda m: m['last_message']['id']):
                cur = msg['last_message']
                if cur['out'] or cur['id'] in self.longpolled_messages:
                    continue
                if self.last_message_id and cur['id'] > self.last_message_id:
                    continue  # wtf?
                res.append(self.message_class(cur, method='getConversations'))
            self.longpolled_messages.clear()
        else:
            res = []
            while not self.longpoll_queue.empty():
                res.append(self.longpoll_queue.get())
            res.sort(key=lambda x: x.id)
            self.longpolled_messages.update(i.id for i in res)
            if res:
                self.last_message_id = max(self.last_message_id, res[-1].id)
        return res

    def _getLongpoll(self):
        arr = self.api.getLongpoll()
        if self.terminate_monitor:
            return []
        need_extra = []
        result = []
        for record in arr:
            if record[0] == 4:  # new message
                lm = LongpollMessage(record[1:])
                if self.longpoll_callback and self.longpoll_callback(lm):
                    continue

                if lm.flags & 2:
                    continue
                msg = {'id': lm.mid, 'date': lm.ts, 'text': html.unescape(lm.text).replace('<br>', '\n'), 'out': 0, '_method': ''}
                if lm.opt.get('source_act'):
                    msg['body'] = None
                    msg['action'] = lm.opt['source_act']
                msg['peer_id'] = lm.sender
                if 'from' in lm.opt:
                    msg['from_id'] = int(lm.opt['from'])
                else:
                    msg['from_id'] = lm.sender

                attachments = []
                for number in range(1, 11):
                    prefix = 'attach' + str(number)
                    kind = lm.extra.get(prefix + '_type')
                    if kind is None:
                        continue  # or break
                    if kind == 'photo':
                        attachments.append({'type': 'photo'})
                    elif kind == 'sticker':
                        attachments.append({'type': 'sticker'})
                    elif kind == 'doc' and lm.extra.get(prefix + '_kind') == 'graffiti':
                        attachments.append({'type': 'graffiti'})
                    elif kind == 'call':
                        attachments.append({'type': 'call'})
                    else:  # something hard
                        need_extra.append(str(lm.mid))
                        msg = None
                        break
                if not msg:
                    continue
                if attachments:
                    msg['attachments'] = attachments
                for i in list(lm.extra):
                    if i.startswith('attach'):
                        del lm.extra[i]
                if not set(lm.extra) <= {'emoji'} and not lm.extra.get('source_act'):
                    need_extra.append(str(lm.mid))
                    continue
                result.append(self.message_class(msg, method='longpoll'))

        if need_extra:
            need_extra = ','.join(need_extra)
            for i in self.api.messages.getById(message_ids=need_extra)['items']:
                result.append(self.message_class(i, method='getById'))
        return result CONF_START = 2000000000 class VkbotMessage(IncomingMessage):

    VOICE_FALLBACK = 'voice'

    def __init__(self, *args, self_id=None, voice_recognizer=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.self_id = self_id
        self.voice_recognizer = voice_recognizer

    def _construct_forwarded_message(self, data):
        return self.__class__(data, self_id=self.self_id)

    def _get_voice_content(self, voice):
        if self.voice_recognizer is None or 'link_mp3' not in voice:
            return self.VOICE_FALLBACK
        text = self.voice_recognizer.get_text(voice['link_mp3'])
        return text if text is not None else self.VOICE_FALLBACK

    def _process_body(self, toplevel=False):
        if self.is_my_message:
            return ''
        if self.action:
            return None

        result = self.body
        att = []
        for a in self.attachments:
            if a['type'] == 'audio':
                if not config.get('vkbot.ignore_audio', 'b'):
                    att.append(a['audio']['title'])
            elif a['type'] == 'video':
                att.append(a['video']['title'])
            elif a['type'] == 'wall':
                att.append(a['wall']['text'])
                if not a['wall']['text'] and 'copy_history' in a['wall']:
                    att[-1] = a['wall']['copy_history'][0]['text']
            elif a['type'] == 'doc':
                att.append(a['doc']['title'])
            elif a['type'] == 'audio_message':
                if toplevel:
                    att.append(self._get_voice_content(a['audio_message']))
                else:
                    att.append(self.VOICE_FALLBACK)
            elif a['type'] == 'gift':
                att.append('vkgift')
            elif a['type'] == 'link':
                att.append(a['link']['title'] + ': ' + a['link']['description'])
            elif a['type'] == 'market':
                att.append(a['market']['description'])
            elif a['type'] == 'sticker':
                att.append('sticker')
            elif a['type'] == 'photo' or a['type'] == 'graffiti':
                result += ' ..'
            elif a['type'] == 'call':
                return None
        for a in att:
            result += ' [' + a + ']'

        if self.fwd_messages:
            fwd_users = {fwd.user_id for fwd in self.fwd_messages}
            if fwd_users in ({self.self_id}, {self.user_id, self.self_id}):
                return result.strip() + ' ' + '{}' * len(self.fwd_messages)
            elif fwd_users == {self.user_id}:
                for fwd in self.fwd_messages:
                    r = fwd._process_body()
                    if r is None:
                        return None
                    result += ' {' + r.strip() + '}'
            else:
                return None

        return result.strip()

    @property
    def is_my_message(self):
        return self.user_id == self.self_id

    @cached_property
    def processed_body(self):
        return self._process_body(True)

    def get_answer_case(self):
        text = self.body
        if text != text.lower():
            return 'normal'
        if not text or not text[0].isalpha() or STARTS_WITH_URL_RE.match(text):
            return None
        return 'lower'

    def get_peer_info(self):
        return PeerInfo(self.user_id, self.chat_id) class PeerInfo:

    def __init__(self, user_id, chat_id=None):
        self.user_id = user_id
        self.chat_id = chat_id

    @property
    def peer_id(self):
        if self.chat_id is not None:
            return CONF_START + self.chat_id
        return self.user_id

    @property
    def is_chat(self):
        return self.chat_id is not None class VoiceRecognizer:

    def __init__(self, wit_token):
        from wit import Wit
        self.wit = Wit(wit_token)

    def get_text(self, mp3_link):
        from wit.wit import WitError
        logger.info('Recognizing voice message')
        try:
            with urllib.request.urlopen(mp3_link) as f:
                return self.wit.speech(f, None, {'Content-type': 'audio/mpeg'})['_text']
        except WitError:
            logger.warning('Wit error')
            return None
import datetime
import html
import json
import logging
import os
import random
import re
import threading
import time
import accounts
import args
import captcha
import config
import log
import stats
import vkapi
import storage
from bot_response import ResponseType, BotResponse
from cache import UserCache, ConfCache, MessageCache
from check_friend import FriendController
from thread_manager import ThreadManager, Timeline
from vkapi import MessageReceiver, CONF_START
from vkbot_message import VkbotMessage, PeerInfo
from voice import VoiceRecognizer
    from vkapi.auth import perms



ignored_errors = {
    # (code, method): (message, can_retry)
    (900, 'messages.send'): ('Blacklisted', False),
    (902, 'messages.send'): ('Unable to reply', False),
    (7, 'messages.send'): ('Banned', False),
    (10, 'messages.send'): ('Unable to reply', True),
    (15, 'friends.delete'): None,
    (15, 'messages.setActivity'): None,
    (917, 'messages.setActivity'): None,
    (100, 'messages.markAsRead'): None,
    (100, 'messages.getHistory'): ('Unable to get message history', True),
    (113, 'users.get'): None,
    (100, 'messages.removeChatUser'): ('Unable to leave', False),
    (15, 'messages.removeChatUser'): ('Already kicked', False),
    (8, '*'): (lambda p, m: '{}: error code 8'.format(m), True),
    (10, '*'): (lambda p, m: '{}: error code 10'.format(m), True),
    (100, 'messages.getChat'): None,
}

def createCaptchaHandler():
    captcha_params = {
        'antigate_key': config.get('captcha.antigate_key'),
        'png_filename': accounts.getFile('captcha.png'),
        'txt_filename': accounts.getFile('captcha.txt'),
        'checks_before_antigate': config.get('captcha.checks_before_antigate', 'i'),
        'check_interval': config.get('captcha.check_interval', 'i'),
    }
    return captcha.CaptchaHandler(captcha_params)

def _getFriendControllerParams():
    conf = config.get('friend_checks')
    return {i: conf.get(i) for i in conf}

def createFriendController():
    controller_params = _getFriendControllerParams()
    return FriendController(controller_params, accounts.getFile('allowed.txt'))

def createVkApi(username, password, ignored_errors=None):
    if not ignored_errors:
        ignored_errors = {}
    v = vkapi.VkApi(ignored_errors=ignored_errors, timeout=config.get('vkbot_timing.default_timeout', 'i'),
                token_file=accounts.getFile('token.txt'),
                log_file=accounts.getFile('inf.log') if args.args['logging'] else '', captcha_handler=createCaptchaHandler())
    v.login_params = {'username': username, 'password': password, 'client_id': config.get('vkapi.client_id'), 'perms': 
                        (perms.FRIENDS | perms.MESSAGES | perms.WALL | perms.OFFLINE | perms.NOTIFICATIONS
                         | perms.PHOTOS | perms.VIDEO | perms.GROUPS)}
    return v


class TimeTracker:
    def __init__(self, size, delay):
        self.times = [0] * size
        self.delay = delay

    def hit(self):
        self.times = self.times[1:] + [time.time()]

    def overload(self):
        return time.time() - self.times[0] < self.delay

class VkBot:
    fields = 'sex,blacklisted,blacklisted_by_me'

    def __init__(self, username='', password='', get_dialogs_interval=60):

        self.delay_on_reply = config.get('vkbot_timing.delay_on_reply', 'i')
        self.chars_per_second = config.get('vkbot_timing.chars_per_second', 'i')
        self.same_user_interval = config.get('vkbot_timing.same_user_interval', 'i')
        self.same_conf_interval = config.get('vkbot_timing.same_conf_interval', 'i')
        self.forget_interval = config.get('vkbot_timing.forget_interval', 'i')
        self.delay_on_first_reply = config.get('vkbot_timing.delay_on_first_reply', 'i')
        self.stats_dialog_count = config.get('stats.dialog_count', 'i')
        self.no_leave_conf = config.get('vkbot.no_leave_conf', 'b')
        self.unfriend_on_invite = config.get('vkbot.unfriend_on_invite', 'b')
        self.unfriend_on_create = config.get('vkbot.unfriend_on_create', 'b')
        self.leave_created_conf = config.get('vkbot.leave_created_conf', 'b')

        self.api = createVkApi(username, password, ignored_errors)
        stats.update('logging', bool(self.api.log_file))
        self.vars = json.load(open('data/defaultvars.json', encoding='utf-8'))
        self.vars['default_bf'] = self.vars['bf']['id']
        self.initSelf(True)
        # hi java

        self.users = UserCache(self.api, self.fields + ',' + FriendController.requiredFields(_getFriendControllerParams()),