<filename>bmbf/poster.py
"""4chan poster."""

import datetime as dt
from io import BytesIO
import logging
import re
from typing import Optional, Tuple

import requests

import bmbf

logger = logging.getLogger(__name__)


class PostingError(RuntimeError):
    """Error when posting message."""

    def __init__(self, status_code: int, text: str) -> None:
        super().__init__(text)
        self.status_code = status_code
        self.text = text


class PassSession(requests.Session):

    def __init__(self, pass_id: str, pass_value: str) -> None:
        super().__init__()

        # Set 4chan pass cookies
        self.cookies.set('4chan_name', 'deleted', domain='4chan.org')
        self.cookies.set('4chan_pass', pass_value, domain='4chan.org')
        self.cookies.set('pass_enabled', '1', domain='4chan.org')
        self.cookies.set('pass_id', pass_id, domain='4chan.org')

        # Set headers
        self.headers['User-Agent'] = f'bmbf/{bmbf.__version__}'

    @staticmethod
    def _since_start_of_time() -> str:
        return dt.datetime(1970, 1, 1).strftime('%a, %d %b %Y %H:%M:%S GMT')

    def prepare_request(self, request: requests.Request) -> requests.PreparedRequest:
        if request.headers is None:
            request.headers = {}
        if request.method in ('GET', 'HEAD'):
            request.headers.setdefault('If-Modified-Since', self._since_start_of_time())
        return super().prepare_request(request)

    def post_message(
            self,
            board: str = 'soc',
            thread_id: Optional[int] = None,
            name: Optional[str] = None,
            email: Optional[str] = None,
            subject: Optional[str] = None,
            comment: Optional[str] = None,
            mode: Optional[str] = 'regist',
            image: Optional[Tuple[str, bytes]] = None,
    ) -> None:
        data = {}

        # Set pass if present
        pwd = self.cookies.get('4chan_pass', domain='4chan.org')
        if pwd is not None:
            data['pwd'] = <PASSWORD>

        for key, value in (
                ('name', name),
                ('email', email),
                ('sub', subject),
                ('com', comment),
                ('resto', thread_id),
                ('mode', mode)
        ):
            if value is not None:
                data[key] = value

        # Add image if present
        if image is not None:
            image_name, image_data = image
            files = {'upfile': (image_name, BytesIO(image_data))}
        else:
            image_name, image_data = None, None
            files = None

        logger.info(
            'Posting to %s/%s: com=%s upfile=%s',
            board, thread_id, comment, image_name
        )
        r = self.post(
            f'https://sys.4chan.org/{board}/post',
            data=data,
            files=files
        )
        if not r.ok or 'Post successful!' not in r.text:
            # Try and find a meaningful error
            match = re.search(r'<span id="errmsg" style="color: red;">(?P<errmsg>.*?)</span>', r.text)
            if match:
                raise PostingError(r.status_code, match.group('errmsg'))
            # Limit to just first table
            match = re.search(r'<table.*?</table>', r.text)
            if match:
                raise PostingError(r.status_code, match.group('errmsg'))
            # Or just take the whole thing...
            raise PostingError(r.status_code, r.text)

<filename>bmbf/poster.py
"""4chan poster."""

import datetime as dt
from io import BytesIO
import logging
import re
from typing import Optional, Tuple

import requests

import bmbf

logger = logging.getLogger(__name__)


class PostingError(RuntimeError):
    """Error when posting message."""

    def __init__(self, status_code: int, text: str) -> None:
        super().__init__(text)
        self.status_code = status_code
        self.text = text


class PassSession(requests.Session):

    def __init__(self, pass_id: str, pass_value: str) -> None:
        super().__init__()

        # Set 4chan pass cookies
        self.cookies.set('4chan_name', 'deleted', domain='4chan.org')
        self.cookies.set('4chan_pass', pass_value, domain='4chan.org')
        self.cookies.set('pass_enabled', '1', domain='4chan.org')
        self.cookies.set('pass_id', pass_id, domain='4chan.org')

        # Set headers
        self.headers['User-Agent'] = f'bmbf/{bmbf.__version__}'

    @staticmethod
    def _since_start_of_time() -> str:
        return dt.datetime(1970, 1, 1).strftime('%a, %d %b %Y %H:%M:%S GMT')

    def prepare_request(self, request: requests.Request) -> requests.PreparedRequest:
        if request.headers is None:
            request.headers = {}
        if request.method in ('GET', 'HEAD'):
            request.headers.setdefault('If-Modified-Since', self._since_start_of_time())
        return super().prepare_request(request)

    def post_message(
            self,
            board: str = 'soc',
            thread_id: Optional[int] = None,
            name: Optional[str] = None,
            email: Optional[str] = None,
            subject: Optional[str] = None,
            comment: Optional[str] = None,
            mode: Optional[str] = 'regist',
            image: Optional[Tuple[str, bytes]] = None,
    ) -> None:
        data = {}

        # Set pass if present
        pwd = self.cookies.get('4chan_pass', domain='4chan.org')
        if pwd is not None:
            data['pwd'] = <PASSWORD>

        for key, value in (
                ('name', name),
                ('email', email),
                ('sub', subject),
                ('com', comment),
                ('resto', thread_id),
                ('mode', mode)
        ):
            if value is not None:
                data[key] = value

        # Add image if present
        if image is not None:
            image_name, image_data = image
            files = {'upfile': (image_name, BytesIO(image_data))}
        else:
            image_name, image_data = None, None
            files = None

        logger.info(
            'Posting to %s/%s: com=%s upfile=%s',
            board, thread_id, comment, image_name
        )
        r = self.post(
            f'https://sys.4chan.org/{board}/post',
            data=data,
            files=files
        )
        if not r.ok or 'Post successful!' not in r.text:
            # Try and find a meaningful error
            match = re.search(r'<span id="errmsg" style="color: red;">(?P<errmsg>.*?)</span>', r.text)
            if match:
                raise PostingError(r.status_code, match.group('errmsg'))
            # Limit to just first table
            match = re.search(r'<table.*?</table>', r.text)
            if match:
                raise PostingError(r.status_code, match.group('errmsg'))
            # Or just take the whole thing...
            raise PostingError(r.status_code, r.text)

4chan poster. Error when posting message. # Set 4chan pass cookies # Set headers # Set pass if present # Add image if present # Try and find a meaningful error # Limit to just first table # Or just take the whole thing...