__filename__ = "outbox.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Timeline"

import os
from shutil import copyfile
from session import createSession
from auth import createPassword
from posts import isImageMedia
from posts import outboxMessageCreateWrap
from posts import savePostToBox
from posts import sendToFollowersThread
from posts import sendToNamedAddressesThread
from utils import hasObjectStringType
from utils import get_base_content_from_post
from utils import has_object_dict
from utils import getLocalNetworkAddresses
from utils import get_full_domain
from utils import removeIdEnding
from utils import getDomainFromActor
from utils import dangerousMarkup
from utils import is_featured_writer
from utils import load_json
from utils import save_json
from utils import acct_dir
from utils import local_actor_url
from utils import hasActor
from blocking import isBlockedDomain
from blocking import outboxBlock
from blocking import outboxUndoBlock
from blocking import outboxMute
from blocking import outboxUndoMute
from media import replaceYouTube
from media import replaceTwitter
from media import getMediaPath
from media import createMediaDirs
from inbox import inboxUpdateIndex
from announce import outboxAnnounce
from announce import outboxUndoAnnounce
from follow import outboxUndoFollow
from follow import followerApprovalActive
from skills import outboxSkills
from availability import outboxAvailability
from like import outboxLike
from like import outboxUndoLike
from reaction import outboxReaction
from reaction import outboxUndoReaction
from bookmarks import outboxBookmark
from bookmarks import outboxUndoBookmark
from delete import outboxDelete
from shares import outboxShareUpload
from shares import outboxUndoShareUpload
from webapp_post import individualPostAsHtml


def _outboxPersonReceiveUpdate(recentPostsCache: {},
                               base_dir: str, http_prefix: str,
                               nickname: str, domain: str, port: int,
                               message_json: {}, debug: bool) -> None:
    """ Receive an actor update from c2s
    For example, setting the PGP key from the desktop client
    """
    # these attachments are updatable via c2s
    updatableAttachments = ('PGP', 'OpenPGP', 'Email')

    if not message_json.get('type'):
        return
    if not isinstance(message_json['type'], str):
        if debug:
            print('DEBUG: c2s actor update type is not a string')
        return
    if message_json['type'] != 'Update':
        return
    if not hasObjectStringType(message_json, debug):
        return
    if not isinstance(message_json['object']['type'], str):
        if debug:
            print('DEBUG: c2s actor update object type is not a string')
        return
    if message_json['object']['type'] != 'Person':
        if debug:
            print('DEBUG: not a c2s actor update')
        return
    if not message_json.get('to'):
        if debug:
            print('DEBUG: c2s actor update has no "to" field')
        return
    if not hasActor(message_json, debug):
        return
    if not message_json.get('id'):
        if debug:
            print('DEBUG: c2s actor update has no id field')
        return
    if not isinstance(message_json['id'], str):
        if debug:
            print('DEBUG: c2s actor update id is not a string')
        return
    domain_full = get_full_domain(domain, port)
    actor = local_actor_url(http_prefix, nickname, domain_full)
    if len(message_json['to']) != 1:
        if debug:
            print('DEBUG: c2s actor update - to does not contain one actor ' +
                  str(message_json['to']))
        return
    if message_json['to'][0] != actor:
        if debug:
            print('DEBUG: c2s actor update - to does not contain actor ' +
                  str(message_json['to']) + ' ' + actor)
        return
    if not message_json['id'].startswith(actor + '#updates/'):
        if debug:
            print('DEBUG: c2s actor update - unexpected id ' +
                  message_json['id'])
        return
    updatedActorJson = message_json['object']
    # load actor from file
    actorFilename = acct_dir(base_dir, nickname, domain) + '.json'
    if not os.path.isfile(actorFilename):
        print('actorFilename not found: ' + actorFilename)
        return
    actor_json = load_json(actorFilename)
    if not actor_json:
        return
    actorChanged = False
    # update fields within actor
    if 'attachment' in updatedActorJson:
        for newPropertyValue in updatedActorJson['attachment']:
            if not newPropertyValue.get('name'):
                continue
            if newPropertyValue['name'] not in updatableAttachments:
                continue
            if not newPropertyValue.get('type'):
                continue
            if not newPropertyValue.get('value'):
                continue
            if newPropertyValue['type'] != 'PropertyValue':
                continue
            if 'attachment' not in actor_json:
                continue
            found = False
            for attachIdx in range(len(actor_json['attachment'])):
                if actor_json['attachment'][attachIdx]['type'] != \
                   'PropertyValue':
                    continue
                if actor_json['attachment'][attachIdx]['name'] != \
                   newPropertyValue['name']:
                    continue
                else:
                    if actor_json['attachment'][attachIdx]['value'] != \
                       newPropertyValue['value']:
                        actor_json['attachment'][attachIdx]['value'] = \
                            newPropertyValue['value']
                        actorChanged = True
                    found = True
                    break
            if not found:
                actor_json['attachment'].append({
                    "name": newPropertyValue['name'],
                    "type": "PropertyValue",
                    "value": newPropertyValue['value']
                })
                actorChanged = True
    # save actor to file
    if actorChanged:
        save_json(actor_json, actorFilename)
        if debug:
            print('actor saved: ' + actorFilename)
    if debug:
        print('New attachment: ' + str(actor_json['attachment']))
    message_json['object'] = actor_json
    if debug:
        print('DEBUG: actor update via c2s - ' + nickname + '@' + domain)


def postMessageToOutbox(session, translate: {},
                        message_json: {}, postToNickname: str,
                        server, base_dir: str, http_prefix: str,
                        domain: str, domain_full: str,
                        onion_domain: str, i2p_domain: str, port: int,
                        recentPostsCache: {}, followers_threads: [],
                        federation_list: [], send_threads: [],
                        postLog: [], cached_webfingers: {},
                        person_cache: {}, allow_deletion: bool,
                        proxy_type: str, version: str, debug: bool,
                        yt_replace_domain: str,
                        twitter_replacement_domain: str,
                        show_published_date_only: bool,
                        allow_local_network_access: bool,
                        city: str, system_language: str,
                        shared_items_federated_domains: [],
                        sharedItemFederationTokens: {},
                        low_bandwidth: bool,
                        signing_priv_key_pem: str,
                        peertube_instances: str, theme: str,
                        max_like_count: int,
                        max_recent_posts: int, cw_lists: {},
                        lists_enabled: str,
                        content_license_url: str) -> bool:
    """post is received by the outbox
    Client to server message post
    https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
    """
    if not message_json.get('type'):
        if debug:
            print('DEBUG: POST to outbox has no "type" parameter')
        return False
    if not message_json.get('object') and message_json.get('content'):
        if message_json['type'] != 'Create':
            # https://www.w3.org/TR/activitypub/#object-without-create
            if debug:
                print('DEBUG: POST to outbox - adding Create wrapper')
            message_json = \
                outboxMessageCreateWrap(http_prefix,
                                        postToNickname,
                                        domain, port,
                                        message_json)

    # check that the outgoing post doesn't contain any markup
    # which can be used to implement exploits
    if has_object_dict(message_json):
        contentStr = get_base_content_from_post(message_json, system_language)
        if contentStr:
            if dangerousMarkup(contentStr, allow_local_network_access):
                print('POST to outbox contains dangerous markup: ' +
                      str(message_json))
                return False

    if message_json['type'] == 'Create':
        if not (message_json.get('id') and
                message_json.get('type') and
                message_json.get('actor') and
                message_json.get('object') and
                message_json.get('to')):
            if not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the id parameter ' +
                          str(message_json))
            elif not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the type parameter ' +
                          str(message_json))
            elif not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the actor parameter ' +
                          str(message_json))
            elif not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the object parameter ' +
                          str(message_json))
            else:
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the "to" parameter ' +
                          str(message_json))
            return False

        # actor should be a string
        if not isinstance(message_json['actor'], str):
            return False

        # actor should look like a url
        if '://' not in message_json['actor'] or \
           '.' not in message_json['actor']:
            return False

        # sent by an actor on a local network address?
        if not allow_local_network_access:
            localNetworkPatternList = getLocalNetworkAddresses()
            for localNetworkPattern in localNetworkPatternList:
                if localNetworkPattern in message_json['actor']:
                    return False

        testDomain, testPort = getDomainFromActor(message_json['actor'])
        testDomain = get_full_domain(testDomain, testPort)
        if isBlockedDomain(base_dir, testDomain):
            if debug:
                print('DEBUG: domain is blocked: ' + message_json['actor'])
            return False
        # replace youtube, so that google gets less tracking data
        replaceYouTube(message_json, yt_replace_domain, system_language)
        # replace twitter, so that twitter posts can be shown without
        # having a twitter account
        replaceTwitter(message_json, twitter_replacement_domain,
                       system_language)
        # https://www.w3.org/TR/activitypub/#create-activity-outbox
        message_json['object']['attributedTo'] = message_json['actor']
        if message_json['object'].get('attachment'):
            attachmentIndex = 0
            attach = message_json['object']['attachment'][attachmentIndex]
            if attach.get('mediaType'):
                fileExtension = 'png'
                mediaTypeStr = \
                    attach['mediaType']

                extensions = {
                    "jpeg": "jpg",
                    "gif": "gif",
                    "svg": "svg",
                    "webp": "webp",
                    "avif": "avif",
                    "audio/mpeg": "mp3",
                    "ogg": "ogg",
                    "mp4": "mp4",
                    "webm": "webm",
                    "ogv": "ogv"
                }
                for matchExt, ext in extensions.items():
                    if mediaTypeStr.endswith(matchExt):
                        fileExtension = ext
                        break

                mediaDir = \
                    base_dir + '/accounts/' + \
                    postToNickname + '@' + domain
                uploadMediaFilename = mediaDir + '/upload.' + fileExtension
                if not os.path.isfile(uploadMediaFilename):
                    del message_json['object']['attachment']
                else:
                    # generate a path for the uploaded image
                    mPath = getMediaPath()
                    mediaPath = mPath + '/' + \
                        createPassword(16).lower() + '.' + fileExtension
                    createMediaDirs(base_dir, mPath)
                    mediaFilename = base_dir + '/' + mediaPath
                    # move the uploaded image to its new path
                    os.rename(uploadMediaFilename, mediaFilename)
                    # change the url of the attachment
                    attach['url'] = \
                        http_prefix + '://' + domain_full + '/' + mediaPath
                    attach['url'] = \
                        attach['url'].replace('/media/',
                                              '/system/' +
                                              'media_attachments/files/')

    permittedOutboxTypes = (
        'Create', 'Announce', 'Like', 'EmojiReact', 'Follow', 'Undo',
        'Update', 'Add', 'Remove', 'Block', 'Delete', 'Skill', 'Ignore'
    )
    if message_json['type'] not in permittedOutboxTypes:
        if debug:
            print('DEBUG: POST to outbox - ' + message_json['type'] +
                  ' is not a permitted activity type')
        return False
    if message_json.get('id'):
        postId = removeIdEnding(message_json['id'])
        if debug:
            print('DEBUG: id attribute exists within POST to outbox')
    else:
        if debug:
            print('DEBUG: No id attribute within POST to outbox')
        postId = None
    if debug:
        print('DEBUG: savePostToBox')
    if message_json['type'] != 'Upgrade':
        outboxName = 'outbox'

        # if this is a blog post or an event then save to its own box
        if message_json['type'] == 'Create':
            if has_object_dict(message_json):
                if message_json['object'].get('type'):
                    if message_json['object']['type'] == 'Article':
                        outboxName = 'tlblogs'

        savedFilename = \
            savePostToBox(base_dir,
                          http_prefix,
                          postId,
                          postToNickname, domain_full,
                          message_json, outboxName)
        if not savedFilename:
            print('WARN: post not saved to outbox ' + outboxName)
            return False

        # save all instance blogs to the news actor
        if postToNickname != 'news' and outboxName == 'tlblogs':
            if '/' in savedFilename:
                if is_featured_writer(base_dir, postToNickname, domain):
                    savedPostId = savedFilename.split('/')[-1]
                    blogsDir = \
                        base_dir + '/accounts/news@' + domain + '/tlblogs'
                    if not os.path.isdir(blogsDir):
                        os.mkdir(blogsDir)
                    copyfile(savedFilename, blogsDir + '/' + savedPostId)
                    inboxUpdateIndex('tlblogs', base_dir,
                                     'news@' + domain,
                                     savedFilename, debug)

                # clear the citations file if it exists
                citationsFilename = \
                    base_dir + '/accounts/' + \
                    postToNickname + '@' + domain + '/.citations.txt'
                if os.path.isfile(citationsFilename):
                    try:
                        os.remove(citationsFilename)
                    except OSError:
                        print('EX: postMessageToOutbox unable to delete ' +
                              citationsFilename)

        # The following activity types get added to the index files
        indexedActivities = (
            'Create', 'Question', 'Note', 'EncryptedMessage', 'Article',
            'Patch', 'Announce'
        )
        if message_json['type'] in indexedActivities:
            indexes = [outboxName, "inbox"]
            selfActor = \
                local_actor_url(http_prefix, postToNickname, domain_full)
            for boxNameIndex in indexes:
                if not boxNameIndex:
                    continue

                # should this also go to the media timeline?
                if boxNameIndex == 'inbox':
                    if isImageMedia(session, base_dir, http_prefix,
                                    postToNickname, domain,
                                    message_json,
                                    translate,
                                    yt_replace_domain,
                                    twitter_replacement_domain,
                                    allow_local_network_access,
                                    recentPostsCache, debug, system_language,
                                    domain_full, person_cache,
                                    signing_priv_key_pem):
                        inboxUpdateIndex('tlmedia', base_dir,
                                         postToNickname + '@' + domain,
                                         savedFilename, debug)

                if boxNameIndex == 'inbox' and outboxName == 'tlblogs':
                    continue

                # avoid duplicates of the message if already going
                # back to the inbox of the same account
                if selfActor not in message_json['to']:
                    # show sent post within the inbox,
                    # as is the typical convention
                    inboxUpdateIndex(boxNameIndex, base_dir,
                                     postToNickname + '@' + domain,
                                     savedFilename, debug)

                    # regenerate the html
                    useCacheOnly = False
                    pageNumber = 1
                    showIndividualPostIcons = True
                    manuallyApproveFollowers = \
                        followerApprovalActive(base_dir,
                                               postToNickname, domain)
                    individualPostAsHtml(signing_priv_key_pem,
                                         False, recentPostsCache,
                                         max_recent_posts,
                                         translate, pageNumber,
                                         base_dir, session,
                                         cached_webfingers,
                                         person_cache,
                                         postToNickname, domain, port,
                                         message_json, None, True,
                                         allow_deletion,
                                         http_prefix, __version__,
                                         boxNameIndex,
                                         yt_replace_domain,
                                         twitter_replacement_domain,
                                         show_published_date_only,
                                         peertube_instances,
                                         allow_local_network_access,
                                         theme, system_language,
                                         max_like_count,
                                         boxNameIndex != 'dm',
                                         showIndividualPostIcons,
                                         manuallyApproveFollowers,
                                         False, True, useCacheOnly,
                                         cw_lists, lists_enabled)

    if outboxAnnounce(recentPostsCache,
                      base_dir, message_json, debug):
        if debug:
            print('DEBUG: Updated announcements (shares) collection ' +
                  'for the post associated with the Announce activity')
    if not server.session:
        print('DEBUG: creating new session for c2s')
        server.session = createSession(proxy_type)
        if not server.session:
            print('ERROR: Failed to create session for postMessageToOutbox')
            return False
    if debug:
        print('DEBUG: sending c2s post to followers')
    # remove inactive threads
    inactiveFollowerThreads = []
    for th in followers_threads:
        if not th.is_alive():
            inactiveFollowerThreads.append(th)
    for th in inactiveFollowerThreads:
        followers_threads.remove(th)
    if debug:
        print('DEBUG: ' + str(len(followers_threads)) +
              ' followers threads active')
    # retain up to 200 threads
    if len(followers_threads) > 200:
        # kill the thread if it is still alive
        if followers_threads[0].is_alive():
            followers_threads[0].kill()
        # remove it from the list
        followers_threads.pop(0)
    # create a thread to send the post to followers
    followersThread = \
        sendToFollowersThread(server.session,
                              base_dir,
                              postToNickname,
                              domain, onion_domain, i2p_domain,
                              port, http_prefix,
                              federation_list,
                              send_threads,
                              postLog,
                              cached_webfingers,
                              person_cache,
                              message_json, debug,
                              version,
                              shared_items_federated_domains,
                              sharedItemFederationTokens,
                              signing_priv_key_pem)
    followers_threads.append(followersThread)

    if debug:
        print('DEBUG: handle any unfollow requests')
    outboxUndoFollow(base_dir, message_json, debug)

    if debug:
        print('DEBUG: handle skills changes requests')
    outboxSkills(base_dir, postToNickname, message_json, debug)

    if debug:
        print('DEBUG: handle availability changes requests')
    outboxAvailability(base_dir, postToNickname, message_json, debug)

    if debug:
        print('DEBUG: handle any like requests')
    outboxLike(recentPostsCache,
               base_dir, http_prefix,
               postToNickname, domain, port,
               message_json, debug)
    if debug:
        print('DEBUG: handle any undo like requests')
    outboxUndoLike(recentPostsCache,
                   base_dir, http_prefix,
                   postToNickname, domain, port,
                   message_json, debug)

    if debug:
        print('DEBUG: handle any emoji reaction requests')
    outboxReaction(recentPostsCache,
                   base_dir, http_prefix,
                   postToNickname, domain, port,
                   message_json, debug)
    if debug:
        print('DEBUG: handle any undo emoji reaction requests')
    outboxUndoReaction(recentPostsCache,
                       base_dir, http_prefix,
                       postToNickname, domain, port,
                       message_json, debug)

    if debug:
        print('DEBUG: handle any undo announce requests')
    outboxUndoAnnounce(recentPostsCache,
                       base_dir, http_prefix,
                       postToNickname, domain, port,
                       message_json, debug)

    if debug:
        print('DEBUG: handle any bookmark requests')
    outboxBookmark(recentPostsCache,
                   base_dir, http_prefix,
                   postToNickname, domain, port,
                   message_json, debug)
    if debug:
        print('DEBUG: handle any undo bookmark requests')
    outboxUndoBookmark(recentPostsCache,
                       base_dir, http_prefix,
                       postToNickname, domain, port,
                       message_json, debug)

    if debug:
        print('DEBUG: handle delete requests')
    outboxDelete(base_dir, http_prefix,
                 postToNickname, domain,
                 message_json, debug,
                 allow_deletion,
                 recentPostsCache)

    if debug:
        print('DEBUG: handle block requests')
    outboxBlock(base_dir, http_prefix,
                postToNickname, domain,
                port,
                message_json, debug)

    if debug:
        print('DEBUG: handle undo block requests')
    outboxUndoBlock(base_dir, http_prefix,
                    postToNickname, domain,
                    port, message_json, debug)

    if debug:
        print('DEBUG: handle mute requests')
    outboxMute(base_dir, http_prefix,
               postToNickname, domain,
               port,
               message_json, debug,
               recentPostsCache)

    if debug:
        print('DEBUG: handle undo mute requests')
    outboxUndoMute(base_dir, http_prefix,
                   postToNickname, domain,
                   port,
                   message_json, debug,
                   recentPostsCache)

    if debug:
        print('DEBUG: handle share uploads')
    outboxShareUpload(base_dir, http_prefix, postToNickname, domain,
                      port, message_json, debug, city,
                      system_language, translate, low_bandwidth,
                      content_license_url)

    if debug:
        print('DEBUG: handle undo share uploads')
    outboxUndoShareUpload(base_dir, http_prefix,
                          postToNickname, domain,
                          port, message_json, debug)

    if debug:
        print('DEBUG: handle actor updates from c2s')
    _outboxPersonReceiveUpdate(recentPostsCache,
                               base_dir, http_prefix,
                               postToNickname, domain, port,
                               message_json, debug)

    if debug:
        print('DEBUG: sending c2s post to named addresses')
        if message_json.get('to'):
            print('c2s sender: ' +
                  postToNickname + '@' + domain + ':' + str(port) +
                  ' recipient: ' + str(message_json['to']))
        else:
            print('c2s sender: ' +
                  postToNickname + '@' + domain + ':' + str(port))
    namedAddressesThread = \
        sendToNamedAddressesThread(server.session, base_dir,
                                   postToNickname,
                                   domain, onion_domain, i2p_domain, port,
                                   http_prefix,
                                   federation_list,
                                   send_threads,
                                   postLog,
                                   cached_webfingers,
                                   person_cache,
                                   message_json, debug,
                                   version,
                                   shared_items_federated_domains,
                                   sharedItemFederationTokens,
                                   signing_priv_key_pem)
    followers_threads.append(namedAddressesThread)
    return True