<commit_before>import os

import mock
from guessit import guessit

import tvsort_sl.utils as utils
from tvsort_sl.conf import check_project_setup, get_conf_file_name
from tvsort_sl.tests.test_base import tv_sort

def setup_function(_):
    utils.create_folder(tv_sort.settings.get('TV_PATH'))
    utils.create_folder(tv_sort.settings.get('DUMMY_PATH'))
    utils.create_folder(tv_sort.settings.get('MOVIES_PATH'))
    utils.create_folder(tv_sort.settings.get('UNSORTED_PATH'))

def teardown_function(_):
    utils.delete_folder(tv_sort.settings.get('TV_PATH'), force=True)
    utils.delete_folder(tv_sort.settings.get('DUMMY_PATH'), force=True)
    utils.delete_folder(tv_sort.settings.get('MOVIES_PATH'), force=True)
    utils.delete_folder(tv_sort.settings.get('UNSORTED_PATH'), force=True)

@mock.patch('requests.post', return_value={'status_code': 200})
def test_main(_):
    new_files_folder = tv_sort.settings.get('UNSORTED_PATH')

    # Add ZIP file
    zip_file_path = tv_sort.settings.get('TEST_ZIP_PATH')
    response = utils.copy_file(zip_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    # Add garbage file
    garbage_file_path = tv_sort.settings.get('TEST_GARBAGE_PATH')
    response = utils.copy_file(garbage_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    garbage_file_path = tv_sort.settings.get('GARBAGE_FILE_DS')
    response = utils.copy_file(garbage_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'error'

    # Add tv-show files
    tv_file_path = tv_sort.settings.get('TEST_TV_PATH')
    response = utils.copy_file(tv_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'
    tv_file_path = tv_sort.settings.get('TEST_TV_2_PATH')
    response = utils.copy_file(tv_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'
    tv_file_path = tv_sort.settings.get('TEST_TV_3_PATH')
    response = utils.copy_file(tv_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    # Add movie file
    movie_file_path = tv_sort.settings.get('TEST_MOVIE')
    response = utils.copy_file(movie_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    # create empty folder
    response = utils.create_folder(tv_sort.settings.get('TEST_FOLDER_IN_UNSORTED'))
    assert response[0][0] == 'info'

    result = tv_sort.run()
    counters = result.get('counters')
    assert counters.get('move_or_copy') == 4
    assert counters.get('delete') == 3

@mock.patch('requests.post', return_value={'status_code': 200})
def test_main_process_running(_):
    dummy_file_path = tv_sort.settings.get('DUMMY_FILE_PATH')
    utils.create_file(dummy_file_path)
    result = tv_sort.run()
    assert 'Proses already running' in result.get('errors')
    response = utils.delete_file(dummy_file_path)
    assert response[0][0] == 'info'

@mock.patch('requests.post', return_value={'status_code': 200})
def test_update_xbmc(_):
    response = utils.update_xbmc(tv_sort.settings.get('KODI_IP'))
    assert response[0][0] == 'info'

@mock.patch('tvsort_sl.utils.is_folder_exists', return_value=False)
def test_no_logs_folder(_):
    conf_files = get_conf_file_name(is_test=True)
    response = check_project_setup(tv_sort.settings, conf_files)
    assert response[0][0] == 'error'

def test_is_file_exists():
    file_path = tv_sort.settings.get('TEST_FILE_PATH')
    assert not utils.is_file_exists(file_path)
    response = utils.create_file(file_path)
    assert response[0][0] == 'info'
    assert 'File was created' in response[0][1]

    assert utils.is_file_exists(file_path)
    response = utils.delete_file(file_path)
    assert response[0][0] == 'info'

def test_replace_space_with_dots_int_input():
    string = 1
    assert utils.transform_to_path_name(string) == '1'

def test_not_tv_show():
    tv_file_name = tv_sort.settings.get('TEST_MOVIE')
    video = guessit(tv_file_name)
    assert not utils.is_tv_show(video)

def test_is_tv_show():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    video = guessit(tv_file_name)
    assert utils.is_tv_show(video)

def test_is_movie():
    tv_file_name = tv_sort.settings.get('TEST_MOVIE')
    video = guessit(tv_file_name)
    assert utils.is_movie(video)

def test_compressed_file():
    file_name = 'test.zip'
    assert utils.is_compressed(file_name, tv_sort.settings.get('EXTENSIONS'))

def test_not_compressed_file():
    file_name = 'test.avi'
    assert not utils.is_compressed(file_name, tv_sort.settings.get('EXTENSIONS'))

def test_file_in_ext_list():
    assert utils.is_file_ext_in_list('zip', tv_sort.extensions.get('COMPRESS'))

def test_file_not_in_ext_list():
    assert not utils.is_file_ext_in_list('avi', tv_sort.extensions.get('COMPRESS'))

def test_garbage_file():
    assert utils.is_garbage_file('.DS_Store', tv_sort.extensions)
    assert not utils.is_garbage_file('test.avi', tv_sort.extensions)

def test_show_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    video = guessit(tv_file_name)
    show_name = utils.get_show_name(video)
    assert show_name == 'House of Cards'

def test_folder_exist():
    folder_path = tv_sort.settings.get('FAKE_PATH')
    assert not utils.is_folder_exists(folder_path)
    response = utils.delete_folder(folder_path)
    assert response[0][0] == 'error'

    folder_path = tv_sort.settings.get('TV_PATH')
    assert utils.is_folder_exists(folder_path)

def test_delete_folder():
    response = utils.delete_folder(tv_sort.settings.get('UNSORTED_PATH'))
    assert response[0][0] == 'info'

    response = utils.create_folder(tv_sort.settings.get('UNSORTED_PATH'))
    assert response[0][0] == 'info'

def test_create_folder():
    response = utils.delete_folder(tv_sort.settings.get('DUMMY_PATH'))
    assert response[0][0] == 'info'

    response = utils.create_folder(tv_sort.settings.get('DUMMY_PATH'))
    assert response[0][0] == 'info'

def test_create_folder_that_already_exists():
    response = utils.create_folder(tv_sort.settings.get('DUMMY_PATH'))
    assert response is None

def test_empty_folder():
    folder_path = tv_sort.settings.get('DUMMY_PATH')
    assert utils.folder_empty(folder_path)

def test_not_empty_folder():
    file_name = 'dummy1.txt'
    folder_path = tv_sort.settings.get('DUMMY_PATH')
    file_path = os.path.join(folder_path, file_name)
    response = utils.create_file(file_path)
    assert response[0][0] == 'info'
    assert not utils.folder_empty(folder_path)

    # Can't delete not empty folder
    response = utils.delete_folder(folder_path)
    assert all(message[0] == 'error' for message in response)

    response = utils.delete_file(file_path)
    assert response[0][0] == 'info'

def test_wrong_series_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    video = guessit(tv_file_name)
    show_name = utils.transform_to_path_name(utils.get_show_name(video))
    utils.add_missing_country(video, show_name)
    assert video.get('country') == 'US'

def test_good_series_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_3_PATH')
    video = guessit(tv_file_name)
    show_name = utils.transform_to_path_name(utils.get_show_name(video))
    assert show_name == 'This.Is.Us'

def test_wrong_country_data_in_series_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_3_PATH')
    video = guessit(tv_file_name)
    assert video.get('country') is None

def test_get_file_ext():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    file_ext = utils.get_file_ext(tv_file_name)
    assert 'mkv' == file_ext

def test_delete_file():
    dummy_file_path = tv_sort.settings.get('DUMMY_FILE_PATH')
    response = utils.create_file(dummy_file_path)
    assert response[0][0] == 'info'

    response = utils.delete_file(dummy_file_path)
    assert response[0][0] == 'info'

def test_delete_file_fail():
    dummy_file_path = tv_sort.settings.get('DUMMY_FILE_PATH')
    response = utils.delete_file(dummy_file_path)
    assert response[0][0] == 'error'
    assert 'Unexpected error:' in response[0][1]

def test_move_file():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    utils.create_file(test_file_path)
    new_path = tv_sort.settings.get('TV_PATH')
    new_test_file_path = os.path.join(new_path, utils.get_file_name(test_file_path))

    response = utils.copy_file(test_file_path, new_path)
    assert response[0][0] == 'info'

    # Clean-up
    response = utils.delete_file(new_test_file_path)
    assert response[0][0] == 'info'

def test_move_file_already_exists():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    new_path = tv_sort.settings.get('TV_PATH')
    new_test_file_path = os.path.join(new_path, utils.get_file_name(test_file_path))

    response = utils.create_file(test_file_path)
    assert response[0][0] == 'info'
    response = utils.copy_file(test_file_path, new_path, move_file=False)
    assert response[0][0] == 'info'

    response = utils.create_file(test_file_path)
    assert response[0][0] == 'info'
    response = utils.copy_file(test_file_path, new_path)
    tv_sort.logger.error(response)
    assert response[0][0] == 'error'

    # Clean-up
    response = utils.delete_file(new_test_file_path)
    assert response[0][0] == 'info'

def test_copy_file():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    utils.create_file(test_file_path)
    new_path = tv_sort.settings.get('TV_PATH')
    new_test_file_path = os.path.join(new_path, utils.get_file_name(test_file_path))
    response = utils.copy_file(test_file_path, new_path, move_file=False)
    assert response[0][0] == 'info'

    # Delete both files
    response = utils.delete_file(test_file_path)
    assert response[0][0] == 'info'
    response = utils.delete_file(new_test_file_path)
    assert response[0][0] == 'info'

def test_copy_file_fail():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    new_path = tv_sort.settings.get('TV_PATH')
    response = utils.copy_file(test_file_path, new_path)
    assert response[0][0] == 'error'

def test_get_folder_name_from_path():
    folder_path = utils.get_folder_path_from_file_path(tv_sort.settings.get('DUMMY_FILE_PATH'))
    assert tv_sort.settings.get('TV_PATH') == folder_path

def test_transform_to_path_name():
    original_text = 'This is a string with space.s and dots.'
    new_text = 'This.Is.A.String.With.Space.S.And.Dots.'
    assert utils.transform_to_path_name(original_text) == new_text
<commit_msg>Minor change in test<commit_after>import os

import mock
from guessit import guessit

import tvsort_sl.utils as utils
from tvsort_sl.conf import check_project_setup, get_conf_file_name
from tvsort_sl.tests.test_base import tv_sort

def setup_function(_):
    utils.create_folder(tv_sort.settings.get('TV_PATH'))
    utils.create_folder(tv_sort.settings.get('DUMMY_PATH'))
    utils.create_folder(tv_sort.settings.get('MOVIES_PATH'))
    utils.create_folder(tv_sort.settings.get('UNSORTED_PATH'))

def teardown_function(_):
    utils.delete_folder(tv_sort.settings.get('TV_PATH'), force=True)
    utils.delete_folder(tv_sort.settings.get('DUMMY_PATH'), force=True)
    utils.delete_folder(tv_sort.settings.get('MOVIES_PATH'), force=True)
    utils.delete_folder(tv_sort.settings.get('UNSORTED_PATH'), force=True)

@mock.patch('requests.post', return_value={'status_code': 200})
def test_main(_):
    new_files_folder = tv_sort.settings.get('UNSORTED_PATH')

    # Add ZIP file
    zip_file_path = tv_sort.settings.get('TEST_ZIP_PATH')
    response = utils.copy_file(zip_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    # Add garbage file
    garbage_file_path = tv_sort.settings.get('TEST_GARBAGE_PATH')
    response = utils.copy_file(garbage_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    # Add tv-show files
    tv_file_path = tv_sort.settings.get('TEST_TV_PATH')
    response = utils.copy_file(tv_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'
    tv_file_path = tv_sort.settings.get('TEST_TV_2_PATH')
    response = utils.copy_file(tv_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'
    tv_file_path = tv_sort.settings.get('TEST_TV_3_PATH')
    response = utils.copy_file(tv_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    # Add movie file
    movie_file_path = tv_sort.settings.get('TEST_MOVIE')
    response = utils.copy_file(movie_file_path, new_files_folder, move_file=False)
    assert response[0][0] == 'info'

    # create empty folder
    response = utils.create_folder(tv_sort.settings.get('TEST_FOLDER_IN_UNSORTED'))
    assert response[0][0] == 'info'

    result = tv_sort.run()
    counters = result.get('counters')
    assert counters.get('move_or_copy') == 4
    assert counters.get('delete') == 3

@mock.patch('requests.post', return_value={'status_code': 200})
def test_main_process_running(_):
    dummy_file_path = tv_sort.settings.get('DUMMY_FILE_PATH')
    utils.create_file(dummy_file_path)
    result = tv_sort.run()
    assert 'Proses already running' in result.get('errors')
    response = utils.delete_file(dummy_file_path)
    assert response[0][0] == 'info'

@mock.patch('requests.post', return_value={'status_code': 200})
def test_update_xbmc(_):
    response = utils.update_xbmc(tv_sort.settings.get('KODI_IP'))
    assert response[0][0] == 'info'

@mock.patch('tvsort_sl.utils.is_folder_exists', return_value=False)
def test_no_logs_folder(_):
    conf_files = get_conf_file_name(is_test=True)
    response = check_project_setup(tv_sort.settings, conf_files)
    assert response[0][0] == 'error'

def test_is_file_exists():
    file_path = tv_sort.settings.get('TEST_FILE_PATH')
    assert not utils.is_file_exists(file_path)
    response = utils.create_file(file_path)
    assert response[0][0] == 'info'
    assert 'File was created' in response[0][1]

    assert utils.is_file_exists(file_path)
    response = utils.delete_file(file_path)
    assert response[0][0] == 'info'

def test_replace_space_with_dots_int_input():
    string = 1
    assert utils.transform_to_path_name(string) == '1'

def test_not_tv_show():
    tv_file_name = tv_sort.settings.get('TEST_MOVIE')
    video = guessit(tv_file_name)
    assert not utils.is_tv_show(video)

def test_is_tv_show():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    video = guessit(tv_file_name)
    assert utils.is_tv_show(video)

def test_is_movie():
    tv_file_name = tv_sort.settings.get('TEST_MOVIE')
    video = guessit(tv_file_name)
    assert utils.is_movie(video)

def test_compressed_file():
    file_name = 'test.zip'
    assert utils.is_compressed(file_name, tv_sort.settings.get('EXTENSIONS'))

def test_not_compressed_file():
    file_name = 'test.avi'
    assert not utils.is_compressed(file_name, tv_sort.settings.get('EXTENSIONS'))

def test_file_in_ext_list():
    assert utils.is_file_ext_in_list('zip', tv_sort.extensions.get('COMPRESS'))

def test_file_not_in_ext_list():
    assert not utils.is_file_ext_in_list('avi', tv_sort.extensions.get('COMPRESS'))

def test_garbage_file():
    assert utils.is_garbage_file('.DS_Store', tv_sort.extensions)
    assert not utils.is_garbage_file('test.avi', tv_sort.extensions)

def test_show_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    video = guessit(tv_file_name)
    show_name = utils.get_show_name(video)
    assert show_name == 'House of Cards'

def test_folder_exist():
    folder_path = tv_sort.settings.get('FAKE_PATH')
    assert not utils.is_folder_exists(folder_path)
    response = utils.delete_folder(folder_path)
    assert response[0][0] == 'error'

    folder_path = tv_sort.settings.get('TV_PATH')
    assert utils.is_folder_exists(folder_path)

def test_delete_folder():
    response = utils.delete_folder(tv_sort.settings.get('UNSORTED_PATH'))
    assert response[0][0] == 'info'

    response = utils.create_folder(tv_sort.settings.get('UNSORTED_PATH'))
    assert response[0][0] == 'info'

def test_create_folder():
    response = utils.delete_folder(tv_sort.settings.get('DUMMY_PATH'))
    assert response[0][0] == 'info'

    response = utils.create_folder(tv_sort.settings.get('DUMMY_PATH'))
    assert response[0][0] == 'info'

def test_create_folder_that_already_exists():
    response = utils.create_folder(tv_sort.settings.get('DUMMY_PATH'))
    assert response is None

def test_empty_folder():
    folder_path = tv_sort.settings.get('DUMMY_PATH')
    assert utils.folder_empty(folder_path)

def test_not_empty_folder():
    file_name = 'dummy1.txt'
    folder_path = tv_sort.settings.get('DUMMY_PATH')
    file_path = os.path.join(folder_path, file_name)
    response = utils.create_file(file_path)
    assert response[0][0] == 'info'
    assert not utils.folder_empty(folder_path)

    # Can't delete not empty folder
    response = utils.delete_folder(folder_path)
    assert all(message[0] == 'error' for message in response)

    response = utils.delete_file(file_path)
    assert response[0][0] == 'info'

def test_wrong_series_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    video = guessit(tv_file_name)
    show_name = utils.transform_to_path_name(utils.get_show_name(video))
    utils.add_missing_country(video, show_name)
    assert video.get('country') == 'US'

def test_good_series_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_3_PATH')
    video = guessit(tv_file_name)
    show_name = utils.transform_to_path_name(utils.get_show_name(video))
    assert show_name == 'This.Is.Us'

def test_wrong_country_data_in_series_name():
    tv_file_name = tv_sort.settings.get('TEST_TV_3_PATH')
    video = guessit(tv_file_name)
    assert video.get('country') is None

def test_get_file_ext():
    tv_file_name = tv_sort.settings.get('TEST_TV_PATH')
    file_ext = utils.get_file_ext(tv_file_name)
    assert 'mkv' == file_ext

def test_delete_file():
    dummy_file_path = tv_sort.settings.get('DUMMY_FILE_PATH')
    response = utils.create_file(dummy_file_path)
    assert response[0][0] == 'info'

    response = utils.delete_file(dummy_file_path)
    assert response[0][0] == 'info'

def test_delete_file_fail():
    dummy_file_path = tv_sort.settings.get('DUMMY_FILE_PATH')
    response = utils.delete_file(dummy_file_path)
    assert response[0][0] == 'error'
    assert 'Unexpected error:' in response[0][1]

def test_move_file():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    utils.create_file(test_file_path)
    new_path = tv_sort.settings.get('TV_PATH')
    new_test_file_path = os.path.join(new_path, utils.get_file_name(test_file_path))

    response = utils.copy_file(test_file_path, new_path)
    assert response[0][0] == 'info'

    # Clean-up
    response = utils.delete_file(new_test_file_path)
    assert response[0][0] == 'info'

def test_move_file_already_exists():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    new_path = tv_sort.settings.get('TV_PATH')
    new_test_file_path = os.path.join(new_path, utils.get_file_name(test_file_path))

    response = utils.create_file(test_file_path)
    assert response[0][0] == 'info'
    response = utils.copy_file(test_file_path, new_path, move_file=False)
    assert response[0][0] == 'info'

    response = utils.create_file(test_file_path)
    assert response[0][0] == 'info'
    response = utils.copy_file(test_file_path, new_path)
    tv_sort.logger.error(response)
    assert response[0][0] == 'error'

    # Clean-up
    response = utils.delete_file(new_test_file_path)
    assert response[0][0] == 'info'

def test_copy_file():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    utils.create_file(test_file_path)
    new_path = tv_sort.settings.get('TV_PATH')
    new_test_file_path = os.path.join(new_path, utils.get_file_name(test_file_path))
    response = utils.copy_file(test_file_path, new_path, move_file=False)
    assert response[0][0] == 'info'

    # Delete both files
    response = utils.delete_file(test_file_path)
    assert response[0][0] == 'info'
    response = utils.delete_file(new_test_file_path)
    assert response[0][0] == 'info'

def test_copy_file_fail():
    test_file_path = tv_sort.settings.get('TEST_FILE_PATH')
    new_path = tv_sort.settings.get('TV_PATH')
    response = utils.copy_file(test_file_path, new_path)
    assert response[0][0] == 'error'

def test_get_folder_name_from_path():
    folder_path = utils.get_folder_path_from_file_path(tv_sort.settings.get('DUMMY_FILE_PATH'))
    assert tv_sort.settings.get('TV_PATH') == folder_path

def test_transform_to_path_name():
    original_text = 'This is a string with space.s and dots.'
    new_text = 'This.Is.A.String.With.Space.S.And.Dots.'
    assert utils.transform_to_path_name(original_text) == new_text
<|endoftext|>


<commit_before># -*- test-case-name: twext.internet.test.test_sendfdport -*-
##
# Copyright (c) 2010-2013 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
Implementation of a TCP/SSL port that uses sendmsg/recvmsg as implemented by
L{twext.python.sendfd}.
"""

from os import close
from errno import EAGAIN, ENOBUFS
from socket import (socketpair, fromfd, error as SocketError, AF_UNIX,
                    SOCK_STREAM, SOCK_DGRAM)

from zope.interface import Interface

from twisted.internet.abstract import FileDescriptor
from twisted.internet.protocol import Protocol, Factory

from twext.python.log import Logger
from twext.python.sendmsg import sendmsg, recvmsg
from twext.python.sendfd import sendfd, recvfd
from twext.python.sendmsg import getsockfam

log = Logger()

class InheritingProtocol(Protocol, object):
    """
    When a connection comes in on this protocol, stop reading and writing, and
    dispatch the socket to another process via its factory.
    """

    def connectionMade(self):
        """
        A connection was received; transmit the file descriptor to another
        process via L{InheritingProtocolFactory} and remove my transport from
        the reactor.
        """
        self.transport.stopReading()
        self.transport.stopWriting()
        skt = self.transport.getHandle()
        self.factory.sendSocket(skt)

class InheritingProtocolFactory(Factory, object):
    """
    An L{InheritingProtocolFactory} is a protocol factory which listens for
    incoming connections in a I{master process}, then sends those connections
    off to be inherited by a I{worker process} via an
    L{InheritedSocketDispatcher}.

    L{InheritingProtocolFactory} is instantiated in the master process.

    @ivar dispatcher: an L{InheritedSocketDispatcher} to use to dispatch
        incoming connections to an appropriate subprocess.

    @ivar description: the string to send along with connections received on
        this factory.
    """

    protocol = InheritingProtocol

    def __init__(self, dispatcher, description):
        self.dispatcher = dispatcher
        self.description = description

    def sendSocket(self, socketObject):
        """
        Send the given socket object on to my dispatcher.
        """
        self.dispatcher.sendFileDescriptor(socketObject, self.description)

class _SubprocessSocket(FileDescriptor, object):
    """
    A socket in the master process pointing at a file descriptor that can be
    used to transmit sockets to a subprocess.

    @ivar outSocket: the UNIX socket used as the sendmsg() transport.
    @type outSocket: L{socket.socket}

    @ivar outgoingSocketQueue: an outgoing queue of sockets to send to the
        subprocess, along with their descriptions (strings describing their
        protocol so that the subprocess knows how to handle them; as of this
        writing, either C{"TCP"} or C{"SSL"})
    @ivar outgoingSocketQueue: a C{list} of 2-tuples of C{(socket-object,
        bytes)}

    @ivar status: a record of the last status message received (via recvmsg)
        from the subprocess: this is an application-specific indication of how
        ready this subprocess is to receive more connections.  A typical usage
        would be to count the open connections: this is what is passed to
    @type status: See L{IStatusWatcher} for an explanation of which methods
        determine this type.

    @ivar dispatcher: The socket dispatcher that owns this L{_SubprocessSocket}
    @type dispatcher: L{InheritedSocketDispatcher}
    """

    def __init__(self, dispatcher, inSocket, outSocket, status):
        FileDescriptor.__init__(self, dispatcher.reactor)
        self.status = status
        self.dispatcher = dispatcher
        self.inSocket = inSocket
        self.outSocket = outSocket   # XXX needs to be set non-blocking by somebody
        #self.fileno = outSocket.fileno
        self.outgoingSocketQueue = []
        self.pendingCloseSocketQueue = []

    def sendSocketToPeer(self, skt, description):
        """
        Enqueue a socket to send to the subprocess.
        """
        self.outgoingSocketQueue.append((skt, description))
        self.startWriting()

    def doRead(self, recvmsg=recvmsg):
        """
        Receive a status / health message and record it.
        """
        try:
            data, _ignore_flags, _ignore_ancillary = recvmsg(self.outSocket.fileno())
        except SocketError, se:
            if se.errno not in (EAGAIN, ENOBUFS):
                raise
        else:
            closeCount = self.dispatcher.statusMessage(self, data)
            for ignored in xrange(closeCount):
                self.pendingCloseSocketQueue.pop(0).close()

    def doWrite(self, sendfd=sendfd):
        """
        Transmit as many queued pending file descriptors as we can.
        """
        while self.outgoingSocketQueue:
            skt, desc = self.outgoingSocketQueue.pop(0)
            try:
                sendfd(self.outSocket.fileno(), skt.fileno(), desc)
            except SocketError, se:
                if se.errno in (EAGAIN, ENOBUFS):
                    self.outgoingSocketQueue.insert(0, (skt, desc))
                    return
                raise

            # Ready to close this socket; wait until it is acknowledged.
            self.pendingCloseSocketQueue.append(skt)

        if not self.outgoingSocketQueue:
            self.stopWriting()

class IStatusWatcher(Interface):
    """
    A provider of L{IStatusWatcher} tracks the I{status messages} reported by
    the worker processes over their control sockets, and computes internal
    I{status values} for those messages.  The I{messages} are individual
    octets, representing one of three operations.  C{0} meaning "a new worker
    process has started, with zero connections being processed", C{+} meaning
    "I have received and am processing your request; I am confirming that my
    requests-being-processed count has gone up by one", and C{-} meaning "I
    have completed processing a request, my requests-being-processed count has
    gone down by one".  The I{status value} tracked by
    L{_SubprocessSocket.status} is an integer, indicating the current
    requests-being-processed value.  (FIXME: the intended design here is
    actually just that all I{this} object knows about is that
    L{_SubprocessSocket.status} is an orderable value, and that this
    C{statusWatcher} will compute appropriate values so the status that I{sorts
    the least} is the socket to which new connections should be directed; also,
    the format of the status messages is only known / understood by the
    C{statusWatcher}, not the L{InheritedSocketDispatcher}.  It's hard to
    explain it in that manner though.)

    @note: the intention of this interface is to eventually provide a broader
        notion of what might constitute 'status', so the above explanation just
        explains the current implementation, in for expediency's sake, rather
        than the somewhat more abstract language that would be accurate.
    """

    def initialStatus(): #@NoSelf
        """
        A new socket was created and added to the dispatcher.  Compute an
        initial value for its status.

        @return: the new status.
        """

    def newConnectionStatus(previousStatus): #@NoSelf
        """
        A new connection was sent to a given socket.  Compute its status based
        on the previous status of that socket.

        @param previousStatus: A status value for the socket being sent work,
            previously returned by one of the methods on this interface.

        @return: the socket's status after incrementing its outstanding work.
        """

    def statusFromMessage(previousStatus, message): #@NoSelf
        """
        A status message was received by a worker.  Convert the previous status
        value (returned from L{newConnectionStatus}, L{initialStatus}, or
        L{statusFromMessage}).

        @param previousStatus: A status value for the socket being sent work,
            previously returned by one of the methods on this interface.

        @return: the socket's status after taking the reported message into
            account.
        """

    def closeCountFromStatus(previousStatus): #@NoSelf
        """
        Based on a status previously returned from a method on this
        L{IStatusWatcher}, determine how many sockets may be closed.

        @return: a 2-tuple of C{number of sockets that may safely be closed},
            C{new status}.
        @rtype: 2-tuple of (C{int}, C{<opaque>})
        """

class InheritedSocketDispatcher(object):
    """
    Used by one or more L{InheritingProtocolFactory}s, this keeps track of a
    list of available sockets that connect to I{worker process}es and sends
    inbound connections to be inherited over those sockets, by those processes.

    L{InheritedSocketDispatcher} is therefore insantiated in the I{master
    process}.

    @ivar statusWatcher: The object which will handle status messages and
        convert them into current statuses, as well as .
    @type statusWatcher: L{IStatusWatcher}
    """

    def __init__(self, statusWatcher):
        """
        Create a socket dispatcher.
        """
        self._subprocessSockets = []
        self.statusWatcher = statusWatcher
        from twisted.internet import reactor
        self.reactor = reactor
        self._isDispatching = False

    @property
    def statuses(self):
        """
        Yield the current status of all subprocess sockets.
        """
        for subsocket in self._subprocessSockets:
            yield subsocket.status

    def statusMessage(self, subsocket, message):
        """
        The status of a connection has changed; update all registered status
        change listeners.
        """
        watcher = self.statusWatcher
        status = watcher.statusFromMessage(subsocket.status, message)
        closeCount, subsocket.status = watcher.closeCountFromStatus(status)
        watcher.statusesChanged(self.statuses)
        return closeCount

    def sendFileDescriptor(self, skt, description):
        """
        A connection has been received.  Dispatch it.

        @param skt: the I{connection socket} (i.e.: not the listening socket)
        @type skt: L{socket.socket}

        @param description: some text to identify to the subprocess's
            L{InheritedPort} what type of transport to create for this socket.
        @type description: C{bytes}
        """
        # We want None to sort after 0 and before 1, so coerce to 0.5 - this
        # allows the master to first schedule all child process that are up but
        # not yet busy ahead of those that are still starting up.
        def sortKey(conn):
            if conn.status is None:
                return 0.5
            else:
                return conn.status
        self._subprocessSockets.sort(key=sortKey)
        selectedSocket = self._subprocessSockets[0]
        selectedSocket.sendSocketToPeer(skt, description)
        # XXX Maybe want to send along 'description' or 'skt' or some
        # properties thereof? -glyph
        selectedSocket.status = self.statusWatcher.newConnectionStatus(
            selectedSocket.status
        )
        self.statusWatcher.statusesChanged(self.statuses)

    def startDispatching(self):
        """
        Start listening on all subprocess sockets.
        """
        self._isDispatching = True
        for subSocket in self._subprocessSockets:
            subSocket.startReading()

    def addSocket(self, socketpair=lambda: socketpair(AF_UNIX, SOCK_DGRAM)):
        """
        Add a C{sendmsg()}-oriented AF_UNIX socket to the pool of sockets being
        used for transmitting file descriptors to child processes.

        @return: a socket object for the receiving side; pass this object's
            C{fileno()} as part of the C{childFDs} argument to
            C{spawnProcess()}, then close it.
        """
        i, o = socketpair()
        i.setblocking(False)
        o.setblocking(False)
        a = _SubprocessSocket(self, i, o, self.statusWatcher.initialStatus())
        self._subprocessSockets.append(a)
        if self._isDispatching:
            a.startReading()
        return i

    def removeSocket(self, skt):
        """
        Removes a previously added socket from the pool of sockets being used
        for transmitting file descriptors to child processes.
        """
        for a in self._subprocessSockets:
            if a.inSocket == skt:
                self._subprocessSockets.remove(a)
                break
        else:
            raise ValueError("Unknown socket: {0}".format(skt))

class InheritedPort(FileDescriptor, object):
    """
    An L{InheritedPort} is an L{IReadDescriptor}/L{IWriteDescriptor} created in
    the I{worker process} to handle incoming connections dispatched via
    C{sendmsg}.
    """

    def __init__(self, fd, transportFactory, protocolFactory):
        """
        @param fd: the file descriptor representing a UNIX socket connected to
            a I{master process}.  We will call C{recvmsg} on this socket to
            receive file descriptors.
        @type fd: C{int}

        @param transportFactory: a 4-argument function that takes the socket
            object produced from the file descriptor, the peer address of that
            socket, the (non-ancillary) data sent along with the incoming file
            descriptor, and the protocol built along with it, and returns an
            L{ITransport} provider.  Note that this should NOT call
            C{makeConnection} on the protocol that it produces, as this class
            will do that.

        @param protocolFactory: an L{IProtocolFactory}
        """
        FileDescriptor.__init__(self)
        self.fd = fd
        self.transportFactory = transportFactory
        self.protocolFactory = protocolFactory
        self.statusQueue = []

    def fileno(self):
        """
        Get the FD number for this socket.
        """
        return self.fd

    def doRead(self):
        """
        A message is ready to read.  Receive a file descriptor from our parent
        process.
        """
        try:
            fd, description = recvfd(self.fd)
        except SocketError, se:
            if se.errno != EAGAIN:
                raise
        else:
            try:
                skt = fromfd(fd, getsockfam(fd), SOCK_STREAM)
                close(fd)       # fromfd() calls dup()
                try:
                    peeraddr = skt.getpeername()
                except SocketError:
                    peeraddr = ('0.0.0.0', 0)
                protocol = self.protocolFactory.buildProtocol(peeraddr)
                transport = self.transportFactory(skt, peeraddr,
                                                  description, protocol)
                protocol.makeConnection(transport)
            except:
                log.failure("doRead()")

    def doWrite(self):
        """
        Write some data.
        """
        while self.statusQueue:
            msg = self.statusQueue.pop(0)
            try:
                sendmsg(self.fd, msg, 0)
            except SocketError, se:
                if se.errno in (EAGAIN, ENOBUFS):
                    self.statusQueue.insert(0, msg)
                    return
                raise
        self.stopWriting()

    def reportStatus(self, statusMessage):
        """
        Report a status message to the L{_SubprocessSocket} monitoring this
        L{InheritedPort}'s health in the master process.
        """
        self.statusQueue.append(statusMessage)
        self.startWriting()
<commit_msg>Turns out the fileno attribute is used after all... re-enabling it.<commit_after># -*- test-case-name: twext.internet.test.test_sendfdport -*-
##
# Copyright (c) 2010-2013 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
Implementation of a TCP/SSL port that uses sendmsg/recvmsg as implemented by
L{twext.python.sendfd}.
"""

from os import close
from errno import EAGAIN, ENOBUFS
from socket import (socketpair, fromfd, error as SocketError, AF_UNIX,
                    SOCK_STREAM, SOCK_DGRAM)

from zope.interface import Interface

from twisted.internet.abstract import FileDescriptor
from twisted.internet.protocol import Protocol, Factory

from twext.python.log import Logger
from twext.python.sendmsg import sendmsg, recvmsg
from twext.python.sendfd import sendfd, recvfd
from twext.python.sendmsg import getsockfam

log = Logger()

class InheritingProtocol(Protocol, object):
    """
    When a connection comes in on this protocol, stop reading and writing, and
    dispatch the socket to another process via its factory.
    """

    def connectionMade(self):
        """
        A connection was received; transmit the file descriptor to another
        process via L{InheritingProtocolFactory} and remove my transport from
        the reactor.
        """
        self.transport.stopReading()
        self.transport.stopWriting()
        skt = self.transport.getHandle()
        self.factory.sendSocket(skt)

class InheritingProtocolFactory(Factory, object):
    """
    An L{InheritingProtocolFactory} is a protocol factory which listens for
    incoming connections in a I{master process}, then sends those connections
    off to be inherited by a I{worker process} via an
    L{InheritedSocketDispatcher}.

    L{InheritingProtocolFactory} is instantiated in the master process.

    @ivar dispatcher: an L{InheritedSocketDispatcher} to use to dispatch
        incoming connections to an appropriate subprocess.

    @ivar description: the string to send along with connections received on
        this factory.
    """

    protocol = InheritingProtocol

    def __init__(self, dispatcher, description):
        self.dispatcher = dispatcher
        self.description = description

    def sendSocket(self, socketObject):
        """
        Send the given socket object on to my dispatcher.
        """
        self.dispatcher.sendFileDescriptor(socketObject, self.description)

class _SubprocessSocket(FileDescriptor, object):
    """
    A socket in the master process pointing at a file descriptor that can be
    used to transmit sockets to a subprocess.

    @ivar outSocket: the UNIX socket used as the sendmsg() transport.
    @type outSocket: L{socket.socket}

    @ivar outgoingSocketQueue: an outgoing queue of sockets to send to the
        subprocess, along with their descriptions (strings describing their
        protocol so that the subprocess knows how to handle them; as of this
        writing, either C{"TCP"} or C{"SSL"})
    @ivar outgoingSocketQueue: a C{list} of 2-tuples of C{(socket-object,
        bytes)}

    @ivar status: a record of the last status message received (via recvmsg)
        from the subprocess: this is an application-specific indication of how
        ready this subprocess is to receive more connections.  A typical usage
        would be to count the open connections: this is what is passed to
    @type status: See L{IStatusWatcher} for an explanation of which methods
        determine this type.

    @ivar dispatcher: The socket dispatcher that owns this L{_SubprocessSocket}
    @type dispatcher: L{InheritedSocketDispatcher}
    """

    def __init__(self, dispatcher, inSocket, outSocket, status):
        FileDescriptor.__init__(self, dispatcher.reactor)
        self.status = status
        self.dispatcher = dispatcher
        self.inSocket = inSocket
        self.outSocket = outSocket   # XXX needs to be set non-blocking by somebody
        self.fileno = outSocket.fileno
        self.outgoingSocketQueue = []
        self.pendingCloseSocketQueue = []

    def sendSocketToPeer(self, skt, description):
        """
        Enqueue a socket to send to the subprocess.
        """
        self.outgoingSocketQueue.append((skt, description))
        self.startWriting()

    def doRead(self, recvmsg=recvmsg):
        """
        Receive a status / health message and record it.
        """
        try:
            data, _ignore_flags, _ignore_ancillary = recvmsg(self.outSocket.fileno())
        except SocketError, se:
            if se.errno not in (EAGAIN, ENOBUFS):
                raise
        else:
            closeCount = self.dispatcher.statusMessage(self, data)
            for ignored in xrange(closeCount):
                self.pendingCloseSocketQueue.pop(0).close()

    def doWrite(self, sendfd=sendfd):
        """
        Transmit as many queued pending file descriptors as we can.
        """
        while self.outgoingSocketQueue:
            skt, desc = self.outgoingSocketQueue.pop(0)
            try:
                sendfd(self.outSocket.fileno(), skt.fileno(), desc)
            except SocketError, se:
                if se.errno in (EAGAIN, ENOBUFS):
                    self.outgoingSocketQueue.insert(0, (skt, desc))
                    return
                raise

            # Ready to close this socket; wait until it is acknowledged.
            self.pendingCloseSocketQueue.append(skt)

        if not self.outgoingSocketQueue:
            self.stopWriting()

class IStatusWatcher(Interface):
    """
    A provider of L{IStatusWatcher} tracks the I{status messages} reported by
    the worker processes over their control sockets, and computes internal
    I{status values} for those messages.  The I{messages} are individual
    octets, representing one of three operations.  C{0} meaning "a new worker
    process has started, with zero connections being processed", C{+} meaning
    "I have received and am processing your request; I am confirming that my
    requests-being-processed count has gone up by one", and C{-} meaning "I
    have completed processing a request, my requests-being-processed count has
    gone down by one".  The I{status value} tracked by
    L{_SubprocessSocket.status} is an integer, indicating the current
    requests-being-processed value.  (FIXME: the intended design here is
    actually just that all I{this} object knows about is that
    L{_SubprocessSocket.status} is an orderable value, and that this
    C{statusWatcher} will compute appropriate values so the status that I{sorts
    the least} is the socket to which new connections should be directed; also,
    the format of the status messages is only known / understood by the
    C{statusWatcher}, not the L{InheritedSocketDispatcher}.  It's hard to
    explain it in that manner though.)

    @note: the intention of this interface is to eventually provide a broader
        notion of what might constitute 'status', so the above explanation just
        explains the current implementation, in for expediency's sake, rather
        than the somewhat more abstract language that would be accurate.
    """

    def initialStatus(): #@NoSelf
        """
        A new socket was created and added to the dispatcher.  Compute an
        initial value for its status.

        @return: the new status.
        """

    def newConnectionStatus(previousStatus): #@NoSelf
        """
        A new connection was sent to a given socket.  Compute its status based
        on the previous status of that socket.

        @param previousStatus: A status value for the socket being sent work,
            previously returned by one of the methods on this interface.

        @return: the socket's status after incrementing its outstanding work.
        """

    def statusFromMessage(previousStatus, message): #@NoSelf
        """
        A status message was received by a worker.  Convert the previous status
        value (returned from L{newConnectionStatus}, L{initialStatus}, or
        L{statusFromMessage}).

        @param previousStatus: A status value for the socket being sent work,
            previously returned by one of the methods on this interface.

        @return: the socket's status after taking the reported message into
            account.
        """

    def closeCountFromStatus(previousStatus): #@NoSelf
        """
        Based on a status previously returned from a method on this
        L{IStatusWatcher}, determine how many sockets may be closed.

        @return: a 2-tuple of C{number of sockets that may safely be closed},
            C{new status}.
        @rtype: 2-tuple of (C{int}, C{<opaque>})
        """

class InheritedSocketDispatcher(object):
    """
    Used by one or more L{InheritingProtocolFactory}s, this keeps track of a
    list of available sockets that connect to I{worker process}es and sends
    inbound connections to be inherited over those sockets, by those processes.

    L{InheritedSocketDispatcher} is therefore insantiated in the I{master
    process}.

    @ivar statusWatcher: The object which will handle status messages and
        convert them into current statuses, as well as .
    @type statusWatcher: L{IStatusWatcher}
    """

    def __init__(self, statusWatcher):
        """
        Create a socket dispatcher.
        """
        self._subprocessSockets = []
        self.statusWatcher = statusWatcher
        from twisted.internet import reactor
        self.reactor = reactor
        self._isDispatching = False

    @property
    def statuses(self):
        """
        Yield the current status of all subprocess sockets.
        """
        for subsocket in self._subprocessSockets:
            yield subsocket.status

    def statusMessage(self, subsocket, message):
        """
        The status of a connection has changed; update all registered status
        change listeners.
        """
        watcher = self.statusWatcher
        status = watcher.statusFromMessage(subsocket.status, message)
        closeCount, subsocket.status = watcher.closeCountFromStatus(status)
        watcher.statusesChanged(self.statuses)
        return closeCount

    def sendFileDescriptor(self, skt, description):
        """
        A connection has been received.  Dispatch it.

        @param skt: the I{connection socket} (i.e.: not the listening socket)
        @type skt: L{socket.socket}

        @param description: some text to identify to the subprocess's
            L{InheritedPort} what type of transport to create for this socket.
        @type description: C{bytes}
        """
        # We want None to sort after 0 and before 1, so coerce to 0.5 - this
        # allows the master to first schedule all child process that are up but
        # not yet busy ahead of those that are still starting up.
        def sortKey(conn):
            if conn.status is None:
                return 0.5
            else:
                return conn.status
        self._subprocessSockets.sort(key=sortKey)
        selectedSocket = self._subprocessSockets[0]
        selectedSocket.sendSocketToPeer(skt, description)
        # XXX Maybe want to send along 'description' or 'skt' or some
        # properties thereof? -glyph
        selectedSocket.status = self.statusWatcher.newConnectionStatus(
            selectedSocket.status
        )
        self.statusWatcher.statusesChanged(self.statuses)

    def startDispatching(self):
        """
        Start listening on all subprocess sockets.
        """
        self._isDispatching = True
        for subSocket in self._subprocessSockets:
            subSocket.startReading()

    def addSocket(self, socketpair=lambda: socketpair(AF_UNIX, SOCK_DGRAM)):
        """
        Add a C{sendmsg()}-oriented AF_UNIX socket to the pool of sockets being
        used for transmitting file descriptors to child processes.

        @return: a socket object for the receiving side; pass this object's
            C{fileno()} as part of the C{childFDs} argument to
            C{spawnProcess()}, then close it.
        """
        i, o = socketpair()
        i.setblocking(False)
        o.setblocking(False)
        a = _SubprocessSocket(self, i, o, self.statusWatcher.initialStatus())
        self._subprocessSockets.append(a)
        if self._isDispatching:
            a.startReading()
        return i

    def removeSocket(self, skt):
        """
        Removes a previously added socket from the pool of sockets being used
        for transmitting file descriptors to child processes.
        """
        for a in self._subprocessSockets:
            if a.inSocket == skt:
                self._subprocessSockets.remove(a)
                break
        else:
            raise ValueError("Unknown socket: {0}".format(skt))

class InheritedPort(FileDescriptor, object):
    """
    An L{InheritedPort} is an L{IReadDescriptor}/L{IWriteDescriptor} created in
    the I{worker process} to handle incoming connections dispatched via
    C{sendmsg}.
    """

    def __init__(self, fd, transportFactory, protocolFactory):
        """
        @param fd: the file descriptor representing a UNIX socket connected to
            a I{master process}.  We will call C{recvmsg} on this socket to
            receive file descriptors.
        @type fd: C{int}

        @param transportFactory: a 4-argument function that takes the socket
            object produced from the file descriptor, the peer address of that
            socket, the (non-ancillary) data sent along with the incoming file
            descriptor, and the protocol built along with it, and returns an
            L{ITransport} provider.  Note that this should NOT call
            C{makeConnection} on the protocol that it produces, as this class
            will do that.

        @param protocolFactory: an L{IProtocolFactory}
        """
        FileDescriptor.__init__(self)
        self.fd = fd
        self.transportFactory = transportFactory
        self.protocolFactory = protocolFactory
        self.statusQueue = []

    def fileno(self):
        """
        Get the FD number for this socket.
        """
        return self.fd

    def doRead(self):
        """
        A message is ready to read.  Receive a file descriptor from our parent
        process.
        """
        try:
            fd, description = recvfd(self.fd)
        except SocketError, se:
            if se.errno != EAGAIN:
                raise
        else:
            try:
                skt = fromfd(fd, getsockfam(fd), SOCK_STREAM)
                close(fd)       # fromfd() calls dup()
                try:
                    peeraddr = skt.getpeername()
                except SocketError:
                    peeraddr = ('0.0.0.0', 0)
                protocol = self.protocolFactory.buildProtocol(peeraddr)
                transport = self.transportFactory(skt, peeraddr,
                                                  description, protocol)
                protocol.makeConnection(transport)
            except:
                log.failure("doRead()")

    def doWrite(self):
        """
        Write some data.
        """
        while self.statusQueue:
            msg = self.statusQueue.pop(0)
            try:
                sendmsg(self.fd, msg, 0)
            except SocketError, se:
                if se.errno in (EAGAIN, ENOBUFS):
                    self.statusQueue.insert(0, msg)
                    return
                raise
        self.stopWriting()

    def reportStatus(self, statusMessage):
        """
        Report a status message to the L{_SubprocessSocket} monitoring this
        L{InheritedPort}'s health in the master process.
        """
        self.statusQueue.append(statusMessage)
        self.startWriting()
<|endoftext|>


<commit_before>"""
    A huge map of every Twitter API endpoint to a function definition in Twython.

    Parameters that need to be embedded in the URL are treated with mustaches, e.g:

    {{version}}, etc

    When creating new endpoint definitions, keep in mind that the name of the mustache
    will be replaced with the keyword that gets passed in to the function at call time.

    i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced
    with 47, instead of defaulting to 1 (said defaulting takes place at conversion time).

    This map is organized the order functions are documented at:
    https://dev.twitter.com/docs/api/1.1
"""

# Base Twitter API url, no need to repeat this junk...
base_url = 'http://api.twitter.com/{{version}}'

api_table = {
    # Timelines
    'getMentionsTimeline': {
        'url': '/statuses/mentions_timeline.json',
        'method': 'GET',
    },
    'getUserTimeline': {
        'url': '/statuses/user_timeline.json',
        'method': 'GET',
    },
    'getHomeTimeline': {
        'url': '/statuses/home_timeline.json',
        'method': 'GET',
    },
    'retweetedOfMe': {
        'url': '/statuses/retweets_of_me.json',
        'method': 'GET',
    },

    # Tweets
    'getRetweets': {
        'url': '/statuses/retweets/{{id}}.json',
        'method': 'GET',
    },
    'showStatus': {
        'url': '/statuses/show/{{id}}.json',
        'method': 'GET',
    },
    'destroyStatus': {
        'url': '/statuses/destroy/{{id}}.json',
        'method': 'POST',
    },
    'updateStatus': {
        'url': '/statuses/update.json',
        'method': 'POST',
    },
    'retweet': {
        'url': '/statuses/retweet/{{id}}.json',
        'method': 'POST',
    },
    # See twython.py for update_status_with_media
    'getOembedTweet': {
        'url': '/statuses/oembed.json',
        'method': 'GET',
    },

    # Search
    'search': {
        'url': '/search/tweets.json',
        'method': 'GET',
    },

    # Direct Messages
    'getDirectMessages': {
        'url': '/direct_messages.json',
        'method': 'GET',
    },
    'getSentMessages': {
        'url': '/direct_messages/sent.json',
        'method': 'GET',
    },
    'getDirectMessage': {
        'url': '/direct_messages/show.json',
        'method': 'GET',
    },
    'destroyDirectMessage': {
        'url': '/direct_messages/destroy/{{id}}.json',
        'method': 'POST',
    },
    'sendDirectMessage': {
        'url': '/direct_messages/new.json',
        'method': 'POST',
    },

    # Friends & Followers
    'getUserIdsOfBlockedRetweets': {
        'url': '/friendships/no_retweets/ids.json',
        'method': 'GET',
    },
    'getFriendsIDs': {
        'url': '/friends/ids.json',
        'method': 'GET',
    },
    'getFollowersIDs': {
        'url': '/followers/ids.json',
        'method': 'GET',
    },
    'lookupFriendships': {
        'url': '/friendships/lookup.json',
        'method': 'GET',
    },
    'getIncomingFriendshipIDs': {
        'url': '/friendships/incoming.json',
        'method': 'GET',
    },
    'getOutgoingFriendshipIDs': {
        'url': '/friendships/outgoing.json',
        'method': 'GET',
    },
    'createFriendship': {
        'url': '/friendships/create.json',
        'method': 'POST',
    },
    'destroyFriendship': {
        'url': '/friendships/destroy.json',
        'method': 'POST',
    },
    'updateFriendship': {
        'url': '/friendships/update.json',
        'method': 'POST',
    },
    'showFriendship': {
        'url': '/friendships/show.json',
        'method': 'GET',
    },
    'getFriendsList': {
        'url': '/friends/list.json',
        'method': 'GET',
    },
    'getFollowersList': {
        'url': '/followers/list.json',
        'method': 'GET',
    },

    # Users
    'getAccountSettings': {
        'url': '/account/settings.json',
        'method': 'GET',
    },
    'verifyCredentials': {
        'url': '/account/verify_credentials.json',
        'method': 'GET',
    },
    'updateAccountSettings': {
        'url': '/account/settings.json',
        'method': 'POST',
    },
    'updateDeliveryService': {
        'url': '/account/update_delivery_device.json',
        'method': 'POST',
    },
    'updateProfile': {
        'url': '/account/update_profile.json',
        'method': 'POST',
    },
    # See twython.py for update_profile_background_image
    'updateProfileColors': {
        'url': '/account/update_profile_colors.json',
        'method': 'POST',
    },
    # See twython.py for update_profile_image
    'listBlocks': {
        'url': '/blocks/list.json',
        'method': 'GET',
    },
    'listBlockIds': {
        'url': '/blocks/ids.json',
        'method': 'GET',
    },
    'createBlock': {
        'url': '/blocks/create/{{id}}.json',
        'method': 'POST',
    },
    'destroyBlock': {
        'url': '/blocks/destroy/{{id}}.json',
        'method': 'POST',
    },
    'lookupUser': {
        'url': '/users/lookup.json',
        'method': 'GET',
    },
    'showUser': {
        'url': '/users/show.json',
        'method': 'GET',
    },
    'searchUsers': {
        'url': '/users/search.json',
        'method': 'GET',
    },
    'getContributees': {
        'url': '/users/contributees.json',
        'method': 'GET',
    },
    'getContributors': {
        'url': '/users/contributors.json',
        'method': 'GET',
    },
    'removeProfileBanner': {
        'url': '/account/remove_profile_banner.json',
        'method': 'POST',
    },
    # See twython.py for update_profile_banner

    # Suggested Users
    'getUserSuggestionsBySlug': {
        'url': '/users/suggestions/{{slug}}.json',
        'method': 'GET',
    },
    'getUserSuggestions': {
        'url': '/users/suggestions.json',
        'method': 'GET',
    },
    'getUserSuggestionsStatusesBySlug': {
        'url': '/users/suggestions/{{slug}}/members.json',
        'method': 'GET',
    },

    # Favorites
    'getFavorites': {
        'url': '/favorites/list.json',
        'method': 'GET',
    },
    'destroyFavorite': {
        'url': '/favorites/destroy.json',
        'method': 'POST',
    },
    'createFavorite': {
        'url': '/favorites/create.json',
        'method': 'POST',
    },

    # Lists
    'showLists': {
        'url': '/lists/list.json',
        'method': 'GET',
    },
    'getListStatuses': {
        'url': '/lists/statuses.json',
        'method': 'GET'
    },
    'deleteListMember': {
        'url': '/lists/members/destroy.json',
        'method': 'POST',
    },
    'getListMemberships': {
        'url': '/lists/memberships.json',
        'method': 'GET',
    },
    'getListSubscribers': {
        'url': '/lists/subscribers.json',
        'method': 'GET',
    },
    'subscribeToList': {
        'url': '/lists/subscribers/create.json',
        'method': 'POST',
    },
    'isListSubscriber': {
        'url': '/lists/subscribers/show.json',
        'method': 'GET',
    },
    'unsubscribeFromList': {
        'url': '/lists/subscribers/destroy.json',
        'method': 'POST',
    },
    'createListMembers': {
        'url': '/lists/members/create_all.json',
        'method': 'POST'
    },
    'isListMember': {
        'url': '/lists/members/show.json',
        'method': 'GET',
    },
    'getListMembers': {
        'url': '/lists/members.json',
        'method': 'GET',
    },
    'addListMember': {
        'url': '/lists/members/create.json',
        'method': 'POST',
    },
    'deleteList': {
        'url': '/lists/destroy.json',
        'method': 'POST',
    },
    'updateList': {
        'url': '/lists/update.json',
        'method': 'POST',
    },
    'createList': {
        'url': '/lists/create.json',
        'method': 'POST',
    },
    'getSpecificList': {
        'url': '/lists/show.json',
        'method': 'GET',
    },
    'getListSubscriptions': {
        'url': '/lists/subscriptions.json',
        'method': 'GET',
    },
    'deleteListMembers': {
        'url': '/lists/members/destroy_all.json',
        'method': 'POST'
    },

    # Saved Searches
    'getSavedSearches': {
        'url': '/saved_searches/list.json',
        'method': 'GET',
    },
    'showSavedSearch': {
        'url': '/saved_searches/show/{{id}}.json',
        'method': 'GET',
    },
    'createSavedSearch': {
        'url': '/saved_searches/create.json',
        'method': 'POST',
    },
    'destroySavedSearch': {
        'url': '/saved_searches/destroy/{{id}}.json',
        'method': 'POST',
    },

    # Places & Geo
    'getGeoInfo': {
        'url': '/geo/id/{{place_id}}.json',
        'method': 'GET',
    },
    'reverseGeocode': {
        'url': '/geo/reverse_geocode.json',
        'method': 'GET',
    },
    'searchGeo': {
        'url': '/geo/search.json',
        'method': 'GET',
    },
    'getSimilarPlaces': {
        'url': '/geo/similar_places.json',
        'method': 'GET',
    },
    'createPlace': {
        'url': '/geo/place.json',
        'method': 'POST',
    },

    # Trends
    'getPlaceTrends': {
        'url': '/trends/place.json',
        'method': 'GET',
    },
    'getAvailableTrends': {
        'url': '/trends/available.json',
        'method': 'GET',
    },
    'getClosestTrends': {
        'url': '/trends/closest.json',
        'method': 'GET',
    },

    # Spam Reporting
    'reportSpam': {
        'url': '/users/report_spam.json',
        'method': 'POST',
    },
}

# from https://dev.twitter.com/docs/error-codes-responses
twitter_http_status_codes = {
    200: ('OK', 'Success!'),
    304: ('Not Modified', 'There was no new data to return.'),
    400: ('Bad Request', 'The request was invalid. An accompanying error message will explain why. This is the status code will be returned during rate limiting.'),
    401: ('Unauthorized', 'Authentication credentials were missing or incorrect.'),
    403: ('Forbidden', 'The request is understood, but it has been refused. An accompanying error message will explain why. This code is used when requests are being denied due to update limits.'),
    404: ('Not Found', 'The URI requested is invalid or the resource requested, such as a user, does not exists.'),
    406: ('Not Acceptable', 'Returned by the Search API when an invalid format is specified in the request.'),
    420: ('Enhance Your Calm', 'Returned by the Search and Trends API when you are being rate limited.'),
    500: ('Internal Server Error', 'Something is broken. Please post to the group so the Twitter team can investigate.'),
    502: ('Bad Gateway', 'Twitter is down or being upgraded.'),
    503: ('Service Unavailable', 'The Twitter servers are up, but overloaded with requests. Try again later.'),
}
<commit_msg>New showOwnedLists method<commit_after>"""
    A huge map of every Twitter API endpoint to a function definition in Twython.

    Parameters that need to be embedded in the URL are treated with mustaches, e.g:

    {{version}}, etc

    When creating new endpoint definitions, keep in mind that the name of the mustache
    will be replaced with the keyword that gets passed in to the function at call time.

    i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced
    with 47, instead of defaulting to 1 (said defaulting takes place at conversion time).

    This map is organized the order functions are documented at:
    https://dev.twitter.com/docs/api/1.1
"""

# Base Twitter API url, no need to repeat this junk...
base_url = 'http://api.twitter.com/{{version}}'

api_table = {
    # Timelines
    'getMentionsTimeline': {
        'url': '/statuses/mentions_timeline.json',
        'method': 'GET',
    },
    'getUserTimeline': {
        'url': '/statuses/user_timeline.json',
        'method': 'GET',
    },
    'getHomeTimeline': {
        'url': '/statuses/home_timeline.json',
        'method': 'GET',
    },
    'retweetedOfMe': {
        'url': '/statuses/retweets_of_me.json',
        'method': 'GET',
    },

    # Tweets
    'getRetweets': {
        'url': '/statuses/retweets/{{id}}.json',
        'method': 'GET',
    },
    'showStatus': {
        'url': '/statuses/show/{{id}}.json',
        'method': 'GET',
    },
    'destroyStatus': {
        'url': '/statuses/destroy/{{id}}.json',
        'method': 'POST',
    },
    'updateStatus': {
        'url': '/statuses/update.json',
        'method': 'POST',
    },
    'retweet': {
        'url': '/statuses/retweet/{{id}}.json',
        'method': 'POST',
    },
    # See twython.py for update_status_with_media
    'getOembedTweet': {
        'url': '/statuses/oembed.json',
        'method': 'GET',
    },

    # Search
    'search': {
        'url': '/search/tweets.json',
        'method': 'GET',
    },

    # Direct Messages
    'getDirectMessages': {
        'url': '/direct_messages.json',
        'method': 'GET',
    },
    'getSentMessages': {
        'url': '/direct_messages/sent.json',
        'method': 'GET',
    },
    'getDirectMessage': {
        'url': '/direct_messages/show.json',
        'method': 'GET',
    },
    'destroyDirectMessage': {
        'url': '/direct_messages/destroy/{{id}}.json',
        'method': 'POST',
    },
    'sendDirectMessage': {
        'url': '/direct_messages/new.json',
        'method': 'POST',
    },

    # Friends & Followers
    'getUserIdsOfBlockedRetweets': {
        'url': '/friendships/no_retweets/ids.json',
        'method': 'GET',
    },
    'getFriendsIDs': {
        'url': '/friends/ids.json',
        'method': 'GET',
    },
    'getFollowersIDs': {
        'url': '/followers/ids.json',
        'method': 'GET',
    },
    'lookupFriendships': {
        'url': '/friendships/lookup.json',
        'method': 'GET',
    },
    'getIncomingFriendshipIDs': {
        'url': '/friendships/incoming.json',
        'method': 'GET',
    },
    'getOutgoingFriendshipIDs': {
        'url': '/friendships/outgoing.json',
        'method': 'GET',
    },
    'createFriendship': {
        'url': '/friendships/create.json',
        'method': 'POST',
    },
    'destroyFriendship': {
        'url': '/friendships/destroy.json',
        'method': 'POST',
    },
    'updateFriendship': {
        'url': '/friendships/update.json',
        'method': 'POST',
    },
    'showFriendship': {
        'url': '/friendships/show.json',
        'method': 'GET',
    },
    'getFriendsList': {
        'url': '/friends/list.json',
        'method': 'GET',
    },
    'getFollowersList': {
        'url': '/followers/list.json',
        'method': 'GET',
    },

    # Users
    'getAccountSettings': {
        'url': '/account/settings.json',
        'method': 'GET',
    },
    'verifyCredentials': {
        'url': '/account/verify_credentials.json',
        'method': 'GET',
    },
    'updateAccountSettings': {
        'url': '/account/settings.json',
        'method': 'POST',
    },
    'updateDeliveryService': {
        'url': '/account/update_delivery_device.json',
        'method': 'POST',
    },
    'updateProfile': {
        'url': '/account/update_profile.json',
        'method': 'POST',
    },
    # See twython.py for update_profile_background_image
    'updateProfileColors': {
        'url': '/account/update_profile_colors.json',
        'method': 'POST',
    },
    # See twython.py for update_profile_image
    'listBlocks': {
        'url': '/blocks/list.json',
        'method': 'GET',
    },
    'listBlockIds': {
        'url': '/blocks/ids.json',
        'method': 'GET',
    },
    'createBlock': {
        'url': '/blocks/create/{{id}}.json',
        'method': 'POST',
    },
    'destroyBlock': {
        'url': '/blocks/destroy/{{id}}.json',
        'method': 'POST',
    },
    'lookupUser': {
        'url': '/users/lookup.json',
        'method': 'GET',
    },
    'showUser': {
        'url': '/users/show.json',
        'method': 'GET',
    },
    'searchUsers': {
        'url': '/users/search.json',
        'method': 'GET',
    },
    'getContributees': {
        'url': '/users/contributees.json',
        'method': 'GET',
    },
    'getContributors': {
        'url': '/users/contributors.json',
        'method': 'GET',
    },
    'removeProfileBanner': {
        'url': '/account/remove_profile_banner.json',
        'method': 'POST',
    },
    # See twython.py for update_profile_banner

    # Suggested Users
    'getUserSuggestionsBySlug': {
        'url': '/users/suggestions/{{slug}}.json',
        'method': 'GET',
    },
    'getUserSuggestions': {
        'url': '/users/suggestions.json',
        'method': 'GET',
    },
    'getUserSuggestionsStatusesBySlug': {
        'url': '/users/suggestions/{{slug}}/members.json',
        'method': 'GET',
    },

    # Favorites
    'getFavorites': {
        'url': '/favorites/list.json',
        'method': 'GET',
    },
    'destroyFavorite': {
        'url': '/favorites/destroy.json',
        'method': 'POST',
    },
    'createFavorite': {
        'url': '/favorites/create.json',
        'method': 'POST',
    },

    # Lists
    'showLists': {
        'url': '/lists/list.json',
        'method': 'GET',
    },
    'getListStatuses': {
        'url': '/lists/statuses.json',
        'method': 'GET'
    },
    'deleteListMember': {
        'url': '/lists/members/destroy.json',
        'method': 'POST',
    },
    'getListMemberships': {
        'url': '/lists/memberships.json',
        'method': 'GET',
    },
    'getListSubscribers': {
        'url': '/lists/subscribers.json',
        'method': 'GET',
    },
    'subscribeToList': {
        'url': '/lists/subscribers/create.json',
        'method': 'POST',
    },
    'isListSubscriber': {
        'url': '/lists/subscribers/show.json',
        'method': 'GET',
    },
    'unsubscribeFromList': {
        'url': '/lists/subscribers/destroy.json',
        'method': 'POST',
    },
    'createListMembers': {
        'url': '/lists/members/create_all.json',
        'method': 'POST'
    },
    'isListMember': {
        'url': '/lists/members/show.json',
        'method': 'GET',
    },
    'getListMembers': {
        'url': '/lists/members.json',
        'method': 'GET',
    },
    'addListMember': {
        'url': '/lists/members/create.json',
        'method': 'POST',
    },
    'deleteList': {
        'url': '/lists/destroy.json',
        'method': 'POST',
    },
    'updateList': {
        'url': '/lists/update.json',
        'method': 'POST',
    },
    'createList': {
        'url': '/lists/create.json',
        'method': 'POST',
    },
    'getSpecificList': {
        'url': '/lists/show.json',
        'method': 'GET',
    },
    'getListSubscriptions': {
        'url': '/lists/subscriptions.json',
        'method': 'GET',
    },
    'deleteListMembers': {
        'url': '/lists/members/destroy_all.json',
        'method': 'POST'
    },
    'showOwnedLists': {
        'url': '/lists/ownerships.json',
        'method': 'GET'
    },

    # Saved Searches
    'getSavedSearches': {
        'url': '/saved_searches/list.json',
        'method': 'GET',
    },
    'showSavedSearch': {
        'url': '/saved_searches/show/{{id}}.json',
        'method': 'GET',
    },
    'createSavedSearch': {
        'url': '/saved_searches/create.json',
        'method': 'POST',
    },
    'destroySavedSearch': {
        'url': '/saved_searches/destroy/{{id}}.json',
        'method': 'POST',
    },

    # Places & Geo
    'getGeoInfo': {
        'url': '/geo/id/{{place_id}}.json',
        'method': 'GET',
    },
    'reverseGeocode': {
        'url': '/geo/reverse_geocode.json',
        'method': 'GET',
    },
    'searchGeo': {
        'url': '/geo/search.json',
        'method': 'GET',
    },
    'getSimilarPlaces': {
        'url': '/geo/similar_places.json',
        'method': 'GET',
    },
    'createPlace': {
        'url': '/geo/place.json',
        'method': 'POST',
    },

    # Trends
    'getPlaceTrends': {
        'url': '/trends/place.json',
        'method': 'GET',
    },
    'getAvailableTrends': {
        'url': '/trends/available.json',
        'method': 'GET',
    },
    'getClosestTrends': {
        'url': '/trends/closest.json',
        'method': 'GET',
    },

    # Spam Reporting
    'reportSpam': {
        'url': '/users/report_spam.json',
        'method': 'POST',
    },
}

# from https://dev.twitter.com/docs/error-codes-responses
twitter_http_status_codes = {
    200: ('OK', 'Success!'),
    304: ('Not Modified', 'There was no new data to return.'),
    400: ('Bad Request', 'The request was invalid. An accompanying error message will explain why. This is the status code will be returned during rate limiting.'),
    401: ('Unauthorized', 'Authentication credentials were missing or incorrect.'),
    403: ('Forbidden', 'The request is understood, but it has been refused. An accompanying error message will explain why. This code is used when requests are being denied due to update limits.'),
    404: ('Not Found', 'The URI requested is invalid or the resource requested, such as a user, does not exists.'),
    406: ('Not Acceptable', 'Returned by the Search API when an invalid format is specified in the request.'),
    420: ('Enhance Your Calm', 'Returned by the Search and Trends API when you are being rate limited.'),
    500: ('Internal Server Error', 'Something is broken. Please post to the group so the Twitter team can investigate.'),
    502: ('Bad Gateway', 'Twitter is down or being upgraded.'),
    503: ('Service Unavailable', 'The Twitter servers are up, but overloaded with requests. Try again later.'),
}
<|endoftext|>


<commit_before>from twisted.words.protocols import irc
from txircd.modbase import Command
from txircd.server import ModuleMessage

# These numerics are defined for use in the IRCv3 SASL documentation
# located at http://ircv3.atheme.org/extensions/sasl-3.1
# The names are entirely made up based on their uses.
irc.RPL_SASLACCOUNT = "900"
irc.RPL_SASLSUCCESS = "903"
irc.ERR_SASLFAILED = "904"
irc.ERR_SASLABORTED = "906"
irc.ERR_SASLALREADYAUTHED = "907"

class Sasl(Command):
    def capRequest(self, user, capability):
        return True

    def capAcknowledge(self, user, capability):
        return False

    def capRequestRemove(self, user, capability):
        return True

    def capAcknowledgeRemove(self, user, capability):
        return False

    def capClear(self, user, capability):
        return True

    def onUse(self, user, data):
        if "sasl_authenticating" not in user.cache:
            mechanism = data["authentication"].upper()
            user.cache["sasl_authenticating"] = [mechanism, ""]
            if "server_sasl_agent" in self.ircd.servconfig and self.ircd.servconfig["server_sasl_agent"]:
                if self.ircd.servconfig["server_sasl_agent"] not in self.ircd.servers:
                    del user.cache["sasl_authenticating"]
                    user.sendMessage(irc.ERR_SASLFAILED, ":SASL authentication failed")
                else:
                    server = self.ircd.servers[self.ircd.servconfig["server_sasl_agent"]]
                    server.callRemote(ModuleMessage, destserver=server.name, type="SASL", args=["start", user.uuid, mechanism])
            elif "sasl_agent" in self.ircd.module_data_cache:
                self.startSaslAuth(user, mechanism)
            else:
                del user.cache["sasl_authenticating"]
                user.sendMessage(irc.ERR_SASLFAILED, ":SASL authentication failed")
        else:
            if "server_sasl_agent" in self.ircd.servconfig and self.ircd.servconfig["server_sasl_agent"]:
                server = self.ircd.servers[self.ircd.servconfig["server_sasl_agent"]]
                server.callRemote(ModuleMessage, destserver=server.name, type="SASL", args=["process", user.uuid, data["authentication"]])
            else:
                self.processSaslAuth(user, data["authentication"])

    def processParams(self, user, params):
        if user.registered == 0:
            user.sendMessage(irc.ERR_ALREADYREGISTRED, ":You may not reregister")
            return {}
        if not params:
            user.sendMessage(irc.ERR_NEEDMOREPARAMS, "AUTHENTICATE", ":Not enough parameters")
            return {}
        if "accountname" in user.metadata["ext"]:
            user.sendMessage(irc.ERR_SASLALREADYAUTHED, ":You have already authenticated")
            return {}
        return {
            "user": user,
            "authentication": params[0]
        }

    def checkInProgress(self, user):
        if "sasl_authenticating" in user.cache:
            del user.cache["sasl_authenticating"]
            user.sendMessage(irc.ERR_SASLABORTED, ":SASL authentication aborted")
        return True

    def sendSuccess(self, user):
        user.sendMessage(irc.RPL_SASLACCOUNT, "{}!{}@{}".format(user.nickname if user.nickname else "unknown", user.username if user.username else "unknown", user.hostname), user.metadata["ext"]["accountname"], ":You are now logged in as {}".format(user.metadata["ext"]["accountname"]))
        user.sendMessage(irc.RPL_SASLSUCCESS, ":SASL authentication successful")
        del user.cache["sasl_authenticating"]
        if user.server != self.ircd.name:
            self.ircd.servers[user.server].callRemote(ModuleMessage, type="SASL", args=["release", user.uuid])

    def sendFailure(self, user):
        user.sendMessage(irc.ERR_SASLFAILED, ":SASL authentication failed")
        del user.cache["sasl_authenticating"]
        if user.server != self.ircd.name:
            self.ircd.servers[user.server].callRemote(ModuleMessage, type="SASL", args=["release", user.uuid])

    def saslMessage(self, command, args):
        if args[1] in self.ircd.userid:
            user = self.ircd.userid[args[1]]
        else:
            return
        if args[0] == "start":
            user.cache["sasl_authenticating"] = [args[2], ""]
            self.startSaslAuth(user, args[2])
        elif args[0] == "process":
            self.processSaslAuth(user, args[2])
        elif args[0] == "release":
            del user.cache["sasl_authenticating"]

    def startSaslAuth(self, user, mechanism):
        result = self.ircd.module_data_cache["sasl_agent"].saslStart(user, mechanism)
        if result == "fail":
            self.sendFailure(user)

    def processSaslAuth(self, user, authstr):
        if authstr == "*": # indicates client is aborting authentication
            self.sendFailure(user)
            return
        if authstr != "+": # A single plus sign as input indicates an empty SASL packet that we shouldn't append to data
            user.cache["sasl_authenticating"][1] += authstr
        if len(authstr) == 400:
            return # A string of length 400 means another line is coming with additional data, so don't process any more yet
        result = self.ircd.module_data_cache["sasl_agent"].saslNext(user, user.cache["sasl_authenticating"][1])
        if result == "done":
            if "accountname" in user.metadata["ext"]:
                self.sendSuccess(user)
                self.ircd.module_data_cache["sasl_agent"].saslDone(user, True)
            else:
                self.sendFailure(user)
                self.ircd.module_data_cache["sasl_agent"].saslDone(user, False)
        elif result == "wait":
            self.ircd.module_data_cache["sasl_agent"].bindSaslResult(user, self.sendSuccess, self.sendFailure)

class Spawner(object):
    def __init__(self, ircd):
        self.ircd = ircd
        self.sasl = None

    def spawn(self):
        self.sasl = Sasl()
        if "cap" not in self.ircd.module_data_cache:
            self.ircd.module_data_cache["cap"] = {}
        self.ircd.module_data_cache["cap"]["sasl"] = self.sasl
        if "sasl_mechanisms" not in self.ircd.module_data_cache:
            self.ircd.module_data_cache["sasl_mechanisms"] = {}
        if "server_sasl_agent" not in self.ircd.servconfig:
            self.ircd.servconfig["sasl_agent"] = "" # default to an internal agent, at least until we get s2s going
        return {
            "commands": {
                "AUTHENTICATE": self.sasl
            },
            "actions": {
                "register": [self.sasl.checkInProgress]
            },
            "server": {
                "SASL": self.sasl.saslMessage
            }
        }

    def cleanup(self):
        del self.ircd.commands["AUTHENTICATE"]
        del self.ircd.module_data_cache["cap"]["sasl"]
        self.ircd.actions["register"].remove(self.sasl.checkInProgress)<commit_msg>Fix s2s SASL messages<commit_after>from twisted.words.protocols import irc
from txircd.modbase import Command
from txircd.server import ModuleMessage

# These numerics are defined for use in the IRCv3 SASL documentation
# located at http://ircv3.atheme.org/extensions/sasl-3.1
# The names are entirely made up based on their uses.
irc.RPL_SASLACCOUNT = "900"
irc.RPL_SASLSUCCESS = "903"
irc.ERR_SASLFAILED = "904"
irc.ERR_SASLABORTED = "906"
irc.ERR_SASLALREADYAUTHED = "907"

class Sasl(Command):
    def capRequest(self, user, capability):
        return True

    def capAcknowledge(self, user, capability):
        return False

    def capRequestRemove(self, user, capability):
        return True

    def capAcknowledgeRemove(self, user, capability):
        return False

    def capClear(self, user, capability):
        return True

    def onUse(self, user, data):
        if "sasl_authenticating" not in user.cache:
            mechanism = data["authentication"].upper()
            user.cache["sasl_authenticating"] = [mechanism, ""]
            if "server_sasl_agent" in self.ircd.servconfig and self.ircd.servconfig["server_sasl_agent"]:
                if self.ircd.servconfig["server_sasl_agent"] not in self.ircd.servers:
                    del user.cache["sasl_authenticating"]
                    user.sendMessage(irc.ERR_SASLFAILED, ":SASL authentication failed")
                else:
                    server = self.ircd.servers[self.ircd.servconfig["server_sasl_agent"]]
                    server.callRemote(ModuleMessage, destserver=server.name, type="SASL", args=["start", user.uuid, mechanism])
            elif "sasl_agent" in self.ircd.module_data_cache:
                self.startSaslAuth(user, mechanism)
            else:
                del user.cache["sasl_authenticating"]
                user.sendMessage(irc.ERR_SASLFAILED, ":SASL authentication failed")
        else:
            if "server_sasl_agent" in self.ircd.servconfig and self.ircd.servconfig["server_sasl_agent"]:
                server = self.ircd.servers[self.ircd.servconfig["server_sasl_agent"]]
                server.callRemote(ModuleMessage, destserver=server.name, type="SASL", args=["process", user.uuid, data["authentication"]])
            else:
                self.processSaslAuth(user, data["authentication"])

    def processParams(self, user, params):
        if user.registered == 0:
            user.sendMessage(irc.ERR_ALREADYREGISTRED, ":You may not reregister")
            return {}
        if not params:
            user.sendMessage(irc.ERR_NEEDMOREPARAMS, "AUTHENTICATE", ":Not enough parameters")
            return {}
        if "accountname" in user.metadata["ext"]:
            user.sendMessage(irc.ERR_SASLALREADYAUTHED, ":You have already authenticated")
            return {}
        return {
            "user": user,
            "authentication": params[0]
        }

    def checkInProgress(self, user):
        if "sasl_authenticating" in user.cache:
            del user.cache["sasl_authenticating"]
            user.sendMessage(irc.ERR_SASLABORTED, ":SASL authentication aborted")
        return True

    def sendSuccess(self, user):
        user.sendMessage(irc.RPL_SASLACCOUNT, "{}!{}@{}".format(user.nickname if user.nickname else "unknown", user.username if user.username else "unknown", user.hostname), user.metadata["ext"]["accountname"], ":You are now logged in as {}".format(user.metadata["ext"]["accountname"]))
        user.sendMessage(irc.RPL_SASLSUCCESS, ":SASL authentication successful")
        del user.cache["sasl_authenticating"]
        if user.server != self.ircd.name:
            self.ircd.servers[user.server].callRemote(ModuleMessage, destserver=user.server, type="SASL", args=["release", user.uuid])

    def sendFailure(self, user):
        user.sendMessage(irc.ERR_SASLFAILED, ":SASL authentication failed")
        del user.cache["sasl_authenticating"]
        if user.server != self.ircd.name:
            self.ircd.servers[user.server].callRemote(ModuleMessage, destserver=user.server, type="SASL", args=["release", user.uuid])

    def saslMessage(self, command, args):
        if args[1] in self.ircd.userid:
            user = self.ircd.userid[args[1]]
        else:
            return
        if args[0] == "start":
            user.cache["sasl_authenticating"] = [args[2], ""]
            self.startSaslAuth(user, args[2])
        elif args[0] == "process":
            self.processSaslAuth(user, args[2])
        elif args[0] == "release":
            del user.cache["sasl_authenticating"]

    def startSaslAuth(self, user, mechanism):
        result = self.ircd.module_data_cache["sasl_agent"].saslStart(user, mechanism)
        if result == "fail":
            self.sendFailure(user)

    def processSaslAuth(self, user, authstr):
        if authstr == "*": # indicates client is aborting authentication
            self.sendFailure(user)
            return
        if authstr != "+": # A single plus sign as input indicates an empty SASL packet that we shouldn't append to data
            user.cache["sasl_authenticating"][1] += authstr
        if len(authstr) == 400:
            return # A string of length 400 means another line is coming with additional data, so don't process any more yet
        result = self.ircd.module_data_cache["sasl_agent"].saslNext(user, user.cache["sasl_authenticating"][1])
        if result == "done":
            if "accountname" in user.metadata["ext"]:
                self.sendSuccess(user)
                self.ircd.module_data_cache["sasl_agent"].saslDone(user, True)
            else:
                self.sendFailure(user)
                self.ircd.module_data_cache["sasl_agent"].saslDone(user, False)
        elif result == "wait":
            self.ircd.module_data_cache["sasl_agent"].bindSaslResult(user, self.sendSuccess, self.sendFailure)

class Spawner(object):
    def __init__(self, ircd):
        self.ircd = ircd
        self.sasl = None

    def spawn(self):
        self.sasl = Sasl()
        if "cap" not in self.ircd.module_data_cache:
            self.ircd.module_data_cache["cap"] = {}
        self.ircd.module_data_cache["cap"]["sasl"] = self.sasl
        if "sasl_mechanisms" not in self.ircd.module_data_cache:
            self.ircd.module_data_cache["sasl_mechanisms"] = {}
        if "server_sasl_agent" not in self.ircd.servconfig:
            self.ircd.servconfig["sasl_agent"] = "" # default to an internal agent, at least until we get s2s going
        return {
            "commands": {
                "AUTHENTICATE": self.sasl
            },
            "actions": {
                "register": [self.sasl.checkInProgress]
            },
            "server": {
                "SASL": self.sasl.saslMessage
            }
        }

    def cleanup(self):
        del self.ircd.commands["AUTHENTICATE"]
        del self.ircd.module_data_cache["cap"]["sasl"]
        self.ircd.actions["register"].remove(self.sasl.checkInProgress)<|endoftext|>