#!/usr/bin/env python
# -*- coding: utf-8 -*-
from .post import Post
from .url import Url
[docs]class Thread(object):
"""Represents a thread.
Attributes:
closed (bool): Whether the thread has been closed.
sticky (bool): Whether this thread is a 'sticky'.
topic (:class:`py8chan.Post`): Topic post of the thread, the OP.
posts (list of :class:`py8chan.Post`): List of all posts in the thread, including the OP.
all_posts (list of :class:`py8chan.Post`): List of all posts in the thread, including the OP and any omitted posts.
url (string): URL of the thread, not including semantic slug.
Undefined Attributes (Not implemented in 8chan API. Do not use.):
replies and images: Infuriatingly, the OP post in a thread
doesn't list how many replies there are in a thread.
semantic_url (string): URL of this post, with the thread's 'semantic' component.
semantic_slug (string): This post's 'semantic slug'.
"""
def __init__(self, board, id):
self._board = board
self._url = Url(board=board.name, https=board.https) # 8chan URL generator
self.id = self.number = self.num = self.no = id
self.topic = None
self.replies = []
self.is_404 = False
self.last_reply_id = 0
self.omitted_posts = 0
self.omitted_images = 0
self.want_update = False
def __len__(self):
return self.num_replies
@property
def _api_url(self):
return self._url.thread_api_url(self.id)
@property
def closed(self):
return self.topic._data.get('closed') == 1
@property
def sticky(self):
return self.topic._data.get('sticky') == 1
# 8chan puts last modified in JSON instead of the HTTP header
@property
def _last_modified(self):
return self.topic._data.get('last-modified')
@classmethod
def _from_request(cls, board, res, id):
if res.status_code == 404:
return None
res.raise_for_status()
return cls._from_json(res.json(), board, id)
@classmethod
def _from_json(cls, json, board, id=None):
t = cls(board, id)
posts = json['posts']
head, rest = posts[0], posts[1:]
t.topic = t.op = Post(t, head)
t.replies.extend(Post(t, p) for p in rest)
t.id = head.get('no', id)
t.num_replies = len(posts) - 1 # There's no "replies" item in OP on 8ch!
t.num_images = 0 # There's no "images" item in OP on 8ch!
t.omitted_images = head.get('omitted_images', 0)
t.omitted_posts = head.get('omitted_posts', 0)
if id is not None:
if not t.replies:
t.last_reply_id = t.topic.post_number
else:
t.last_reply_id = t.replies[-1].post_number
else:
t.want_update = True
return t
[docs] def files(self):
"""Returns the URLs of all files attached to posts in the thread."""
# yield all from the topic
if self.topic.has_file:
for item in self.topic.all_files():
yield item.file_url
# yield all from other posts
for reply in self.replies:
if reply.has_file:
for item in reply.all_files():
yield item.file_url
[docs] def thumbs(self):
"""Returns the URLs of all thumbnails in the thread."""
# yield all from the topic
if self.topic.has_file:
for item in self.topic.all_files():
yield item.thumbnail_url
# yield all from other posts
for reply in self.replies:
if reply.has_file:
for item in reply.all_files():
yield item.thumbnail_url
[docs] def filenames(self):
"""Returns the filenames of all files attached to posts in the thread."""
# yield all from the topic
if self.topic.has_file:
for item in self.topic.all_files():
yield item.filename
# yield all from other posts
for reply in self.replies:
if reply.has_file:
for item in reply.all_files():
yield item.filename
[docs] def thumbnames(self):
"""Returns the filenames of all thumbnails in the thread."""
# yield all from the topic
if self.topic.has_file:
for item in self.topic.all_files():
yield item.thumbnail_fname
# yield all from other posts
for reply in self.replies:
if reply.has_file:
for item in reply.all_files():
yield item.thumbnail_fname
def file_objects(self):
"""Returns the :class:`py8chan.File` objects of all files attached to posts in the thread."""
# yield all from the topic
if self.topic.has_file:
for item in self.topic.all_files():
yield item
# yield all from other posts
for reply in self.replies:
if reply.has_file:
for item in reply.all_files():
yield item
[docs] def update(self, force=False):
"""Fetch new posts from the server.
Arguments:
force (bool): Force a thread update, even if thread has 404'd.
Returns:
int: How many new posts have been fetched.
"""
# The thread has already 404'ed, this function shouldn't do anything anymore.
if self.is_404 and not force:
return 0
if self._last_modified:
headers = {'If-Modified-Since': self._last_modified}
else:
headers = None
# random connection errors, just return 0 and try again later
try:
res = self._board._requests_session.get(self._api_url, headers=headers)
except:
# try again later
return 0
# 304 Not Modified, no new posts.
if res.status_code == 304:
return 0
# 404 Not Found, thread died.
elif res.status_code == 404:
self.is_404 = True
# remove post from cache, because it's gone.
self._board._thread_cache.pop(self.id, None)
return 0
elif res.status_code == 200:
# If we somehow 404'ed, we should put ourself back in the cache.
if self.is_404:
self.is_404 = False
self._board._thread_cache[self.id] = self
# Remove
self.want_update = False
self.omitted_images = 0
self.omitted_posts = 0
posts = res.json()['posts']
original_post_count = len(self.replies)
self.topic = Post(self, posts[0])
if self.last_reply_id and not force:
self.replies.extend(Post(self, p) for p in posts if p['no'] > self.last_reply_id)
else:
self.replies[:] = [Post(self, p) for p in posts[1:]]
new_post_count = len(self.replies)
post_count_delta = new_post_count - original_post_count
if not post_count_delta:
return 0
self.last_reply_id = self.replies[-1].post_number
return post_count_delta
else:
res.raise_for_status()
[docs] def expand(self):
"""If there are omitted posts, update to include all posts."""
if self.omitted_posts > 0:
self.update()
@property
def posts(self):
return [self.topic] + self.replies
@property
def all_posts(self):
self.expand()
return self.posts
@property
def https(self):
return self._board._https
@property
def url(self):
return self._url.thread_url(self.id)
# 8chan/vichan does not use semantic urls
@property
def semantic_url(self):
raise AttributeError( "'py8chan.Thread' object has no attribute 'semantic_url'" )
# 8chan/vichan does not use semantic slugs
@property
def semantic_slug(self):
raise AttributeError( "'py8chan.Thread' object has no attribute 'semantic_slug'" )
def __repr__(self):
extra = ''
if self.omitted_images or self.omitted_posts:
extra = ', %i omitted images, %i omitted posts' % (
self.omitted_images, self.omitted_posts
)
return '<Thread /%s/%i, %i replies%s>' % (
self._board.name, self.id, len(self.replies), extra
)