#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=redefined-builtin
from __future__ import unicode_literals
import warnings
from hashlib import md5
from . import utils
from .compat import PY2, text
__all__ = [
'Post', 'Download', 'Comment', 'Blog', 'StreamItem', 'UserInfo',
'Poll', 'TalkItem', 'ActivityItem',
]
[документация]class Post(object):
"""Пост.
Поля ``comments_new_count`` и ``favourited`` устарели; используйте контекст вместо них.
Дополнительные значения контекста:
* ``can_comment`` (True/False) — можно ли отправить комментарий
* ``can_edit`` (True/False) — можно ли редактировать пост
* ``can_delete`` — можно ли удалить пост
* ``can_vote`` — можно ли голосовать за пост
* ``vote_value`` (-1/0/1/None) — голос текущего пользователя
* ``subscribed_to_comments`` (True/False) — подписан ли текущий пользователь на новые комментарии
* ``unread_comments_count`` (int) — число новых комментариев (для постов из списка постов, иначе ноль)
* ``favourited`` (True/False) — добавлен ли пост в избранное
* ``favourite_tags`` (list) — теги избранного поста
* ``can_save_favourite_tags`` (True/False) — можно ли редактировать теги избранного поста (обычно совпадает с ``favourited``)
"""
def __init__(self, time, blog, post_id, author, title, draft,
vote_count, vote_total, body, tags, comments_count=None, comments_new_count=None,
short=False, private=False, blog_name=None, poll=None, favourite=0, favourited=None,
download=None, utctime=None, raw_body=None, cut_text=None, context=None):
self._time = time
self.blog = text(blog) if blog else None
self.post_id = int(post_id)
self.author = text(author)
self.title = text(title)
self.draft = bool(draft)
self.vote_count = int(vote_count) if vote_count is not None else None
self.vote_total = int(vote_total) if vote_total is not None else None
self.tags = tags
self.comments_count = int(comments_count) if comments_count is not None else None
self.short = bool(short)
self.private = bool(private)
self.blog_name = text(blog_name) if blog_name else None
self.poll = poll or None
self.favourite = int(favourite) if favourite is not None else None
if download and (not isinstance(download, Download) or download.post_id != self.post_id):
raise ValueError
self.download = download
self.utctime = utctime
self.cut_text = text(cut_text) if cut_text else None
self.context = context or {}
self.body, self.raw_body = utils.normalize_body(body, raw_body, cls='topic-content text')
if self.short != (self.cut_text is not None):
utils.logger.warning('Post %d: self.short != (self.cut_text is not None)! If you don\'t use tabun_api.Post constructor directly, please report to andreymal.', post_id)
if favourited is not None:
warnings.warn('Post(favourited=...) is deprecated; use context["favourited"] instead of it', FutureWarning, stacklevel=2)
self.context['favourited'] = bool(favourited)
if comments_new_count is not None:
warnings.warn('Post(comments_new_count=...) is deprecated; use context["unread_comments_count"] instead of it', FutureWarning, stacklevel=2)
self.context['unread_comments_count'] = comments_new_count
if not context:
print('WARNING: post', post_id, 'created without context')
elif not context.get('http_host'):
print('WARNING: post', post_id, 'created without context.http_host')
def __repr__(self):
o = "<post " + (self.blog or "[personal]") + '/' + text(self.post_id) + ">"
return o.encode('utf-8') if PY2 else o
def __str__(self):
return self.__repr__()
def __unicode__(self):
return self.__repr__().decode('utf-8', 'replace')
[документация] def hashsum(self, fields=None, debug=False):
"""Считает md5-хэш от конкатенации полей поста (в utf-8), разделённых нулевым байтом.
Поддерживаются только следующие поля:
post_id, time (в UTC, в формате ``%Y-%m-%dT%H:%M:%SZ``), draft, author, blog, title,
cut_text, body (как необработанный html), tags.
По умолчанию используются все они. Если требуется одинаковость хэшей независимо
от версии tabun_api, рекомендуется прописать список полей явно.
Аргумент ``fields`` — список полей для использования
(или любая другая коллекция, для которой работает проверка ``if field in fields``).
Порядок и повторения полей в этом списке значения не имеют. Неизвестные поля игнорируются.
При ``debug=True`` вместо хэша возвращается сырой список, используемый перед хэшированием,
что позволит проверить правильность выбора полей.
Возможные применения хэша —
отслеживание изменений поста (но не мета-информации вроде названия блога и числа голосов)
и идентификация разных версий постов.
"""
buf = []
# Not used: vote_count vote_total comments_count private blog_name poll favourite download context
if fields is None or 'post_id' in fields:
buf.append(text(self.post_id))
if fields is None or 'time' in fields:
buf.append(text(self.utctime.strftime('%Y-%m-%dT%H:%M:%SZ')))
if fields is None or 'draft' in fields:
buf.append('1' if self.draft else '0')
for field in ('author', 'blog', 'title', 'cut_text'):
if fields is None or field in fields:
buf.append(getattr(self, field, None) or '')
if fields is None or 'body' in fields:
buf.append(self.raw_body)
if fields is None or 'tags' in fields:
buf.extend(self.tags)
buf = [x.encode('utf-8') for x in buf]
if debug:
return buf
h = md5(b'\x00'.join(buf))
return h.hexdigest()
@property
def url(self):
host = self.context.get('http_host')
if not host:
raise ValueError('http_host is not available')
return host + '/blog/' + ((self.blog + '/') if self.blog else '') + text(self.post_id) + '.html'
@property
def favourited(self):
warnings.warn('post.favourited is deprecated; use post.context.get("favourited") instead of it', FutureWarning, stacklevel=2)
return self.context.get('favourited')
@favourited.setter
def favourited(self, value):
warnings.warn('post.favourited is deprecated; use post.context.get("favourited") instead of it', FutureWarning, stacklevel=2)
self.context['favourited'] = value
@property
def comments_new_count(self):
warnings.warn('post.comments_new_count is deprecated; use post.context.get("unread_comments_count") instead of it', FutureWarning, stacklevel=2)
return self.context.get('unread_comments_count')
@comments_new_count.setter
def comments_new_count(self, value):
warnings.warn('post.comments_new_count is deprecated; use post.context.get("unread_comments_count") instead of it', FutureWarning, stacklevel=2)
self.context['unread_comments_count'] = value
@property
def time(self):
warnings.warn('post.time is deprecated; use post.utctime instead of it', FutureWarning, stacklevel=2)
return self._time
@time.setter
def time(self, value):
warnings.warn('post.time is deprecated; use post.utctime instead of it', FutureWarning, stacklevel=2)
self._time = value
[документация]class Download(object):
"""Прикрепленный к посту файл (в новом Табуне) или ссылка (в старом Табуне)."""
def __init__(self, type, post_id, filename, count, filesize=None):
self.type = text(type)
if self.type not in ("file", "link"):
raise ValueError
self.post_id = int(post_id)
self.filename = text(filename) if filename else None # или ссылка
self.filesize = int(filesize) if filesize is not None else None # в байтах
self.count = int(count)
[документация]class Blog(object):
"""Блог."""
OPEN = 0
CLOSED = 1
HALFCLOSED = 2
def __init__(self, blog_id, blog, name, creator, readers=0, rating=0.0, status=0,
description=None, admins=None, moderators=None, vote_count=-1, posts_count=-1,
created=None, avatar=None, raw_description=None, context=None, closed=None):
self.blog_id = int(blog_id)
self.blog = text(blog)
self.name = text(name)
self.creator = text(creator)
self.readers = int(readers)
self.rating = float(rating)
self.status = int(status)
self.admins = admins
self.moderators = moderators
self.vote_count = int(vote_count)
self.posts_count = int(posts_count)
self.created = created
self.avatar = text(avatar) if avatar else None
self.context = context or {}
self.description, self.raw_description = utils.normalize_body(description, raw_description, cls='blog-content text')
if closed is not None:
warnings.warn('Blog(closed=...) is deprecated; use status instead of it', FutureWarning, stacklevel=2)
self.status = self.CLOSED if closed else self.OPEN
def __repr__(self):
o = "<blog " + self.blog + ">"
return o.encode('utf-8') if PY2 else o
def __str__(self):
return self.__repr__()
def __unicode__(self):
return self.__repr__().decode('utf-8', 'replace')
@property
def url(self):
host = self.context.get('http_host')
if not host:
raise ValueError('http_host is not available')
return host + '/blog/' + self.blog + '/'
@property
def closed(self):
warnings.warn('blog.closed is deprecated; use blog.status instead of it', FutureWarning, stacklevel=2)
return self.status != self.OPEN
@closed.setter
def closed(self, value):
warnings.warn('blog.closed is deprecated; use blog.status instead of it', FutureWarning, stacklevel=2)
self.status = self.CLOSED if value else self.OPEN
[документация]class StreamItem(object):
"""Элемент «Прямого эфира»."""
def __init__(self, blog, blog_title, title, author, comment_id, comments_count):
self.blog = text(blog) if blog else None
self.blog_title = text(blog_title)
self.title = text(title)
self.author = text(author)
self.comment_id = int(comment_id)
self.comments_count = int(comments_count)
def __repr__(self):
o = "<stream_item " + ((self.blog + "/") if self.blog else '') + text(self.comment_id) + ">"
return o.encode('utf-8') if PY2 else o
def __str__(self):
return self.__repr__()
def __unicode__(self):
return self.__repr__().decode('utf-8')
[документация]class UserInfo(object):
"""Информация о броняше.
full=True, только если информация получена со страницы ``/profile/username/``.
Если private_profile=True, то пользователь скрыл свой профиль настройками
приватности и почти вся информация недоступна.
Если private_profile=False, но private_profile_data=True, то недоступна
только информация из блока «Активность» и фото.
Словарь ``counts`` может быть пустым или содержать None или, если доступно,
содержать следующие значения:
* ``publications`` — число публикаций, учитываются посты, комментарии и
(для своего профиля) заметки;
* ``posts`` — число опубликованных постов (со страницы
``profile/username/created/topics или comments/``);
* ``comments`` — число опубликованных комментариев (со страницы
``profile/username/created/topics или comments или notes/``);
* ``notes`` — число заметок к пользователям (со страницы
``profile/username/created/topics или comments или notes/``);
* ``favourites`` — число добавлений в избранное;
* ``favourites_posts`` — число постов в избранном (со страницы
``profile/username/favourites/topics или comments/``);
* ``favourites_comments`` — число комментариев в избранном (со страницы
``profile/username/favourites/topics или comments/``);
* ``friends`` — число друзей.
Дополнительные значения контекста:
* ``note`` (строка или None) — заметка, оставленная текущим пользователем
* ``can_edit_note`` (True/False/None) — можно ли редактировать заметку
(определяется по наличию формы на странице /profile/foo/)
* ``can_vote`` (True/False/None) — можно ли голосовать за пользователя (изменить рейтинг)
(из-за багов лайвстрита корректно работает только на /profile/foo/)
* ``vote_value`` (1/-1/None) — плюс (1), минус (-1) или голос ещё не оставлен (None)
(из-за багов лайвстрита корректно работает только на /profile/foo/)
"""
def __init__(self, user_id, username, realname, skill, rating, userpic=None, foto=None,
gender=None, birthday=None, registered=None, last_activity=None,
description=None, blogs=None, rating_vote_count=None, contacts=None,
counts=None, full=False, context=None, raw_description=None,
private_profile=False, private_profile_data=False):
self.user_id = int(user_id)
self.username = text(username)
self.realname = text(realname) if realname else None
self.skill = float(skill)
self.rating = float(rating)
self.userpic = text(userpic) if userpic else None
self.foto = text(foto) if foto else None
self.gender = gender if gender in ('M', 'F') else None
self.birthday = birthday
self.registered = registered
self.last_activity = last_activity
self.blogs = {}
self.blogs['owner'] = blogs.get('owner', []) if blogs else []
self.blogs['admin'] = blogs.get('admin', []) if blogs else []
self.blogs['moderator'] = blogs.get('moderator', []) if blogs else []
self.blogs['member'] = blogs.get('member', []) if blogs else []
self.description, self.raw_description = utils.normalize_body(description, raw_description)
self.rating_vote_count = rating_vote_count
self.contacts = contacts
self.counts = counts or {}
self.full = bool(full)
self.context = context or {}
self.private_profile = bool(private_profile)
self.private_profile_data = bool(private_profile_data)
def __repr__(self):
o = "<userinfo " + self.username + ">"
return o.encode('utf-8') if PY2 else o
def __str__(self):
return self.__repr__()
def __unicode__(self):
return self.__repr__().decode('utf-8', 'replace')
@property
def url(self):
host = self.context.get('http_host')
if not host:
raise ValueError('http_host is not available')
return host + '/profile/' + self.username + '/'
[документация]class Poll(object):
"""Опрос. Список items содержит кортежи (название ответа, процент проголосовавших, число проголосовавших)."""
def __init__(self, total, notvoted, items):
self.total = int(total)
self.notvoted = int(notvoted)
self.items = []
for x in items:
self.items.append((text(x[0]), float(x[1]), int(x[2])))
[документация]class TalkItem(object):
"""Личное сообщение. При чтении списка сообщений некоторые поля могут быть None.
Учтите, что для нового письма unread = True и context['unread_comments_count'] = 0.
``recipients_inactive`` — подмножество ``recipients``, содержащее имена пользователей,
удаливших свою копию сообщения.
Дополнительные параметры контекста:
* ``favourited`` (True/False): добавлено ли письмо в избранное
* ``last_is_incoming`` (True/False): является ли последний комментарий
входящим (True) или исходящим (False) (только для списка писем)
* ``unread_comments_count``: число непрочитанных комментариев в письме
(только для списка писем)
"""
def __init__(
self, talk_id, recipients, unread, title, date,
body=None, author=None, comments=None, utctime=None,
recipients_inactive=(), comments_count=0, raw_body=None, context=None,
):
self.talk_id = int(talk_id)
self.recipients = [text(x) for x in recipients]
self.recipients_inactive = [text(x) for x in recipients_inactive]
self.unread = bool(unread)
self.title = text(title)
self.date = date
self.author = text(author) if author else None
self.comments = comments if comments else {}
self.utctime = utctime
self.comments_count = int(comments_count)
self.context = context
self.body, self.raw_body = utils.normalize_body(body, raw_body)
def __repr__(self):
o = "<talk " + text(self.talk_id) + ">"
return o.encode('utf-8') if PY2 else o
def __str__(self):
return self.__repr__()
def __unicode__(self):
return self.__repr__().decode('utf-8', 'replace')
[документация]class ActivityItem(object):
"""Событие со страницы /stream/.
Типы события (``obj.type``):
* ``ActivityItem.WALL_ADD`` — добавление записи на стену пользователя (на Табуне отсутствует)
* ``ActivityItem.POST_ADD`` — добавление поста
* ``ActivityItem.COMMENT_ADD`` — добавление комментария
* ``ActivityItem.BLOG_ADD`` — создание блога
* ``ActivityItem.POST_VOTE`` — голосование за пост
* ``ActivityItem.COMMENT_VOTE`` — голосование за комментарий
* ``ActivityItem.BLOG_VOTE`` — голосование за блог
* ``ActivityItem.USER_VOTE`` — голосование за пользователя (оценивающий в поле ``username``, оцениваемый — в ``data``)
* ``ActivityItem.FRIEND_ADD`` — добавление друга (добавляющий в поле ``username``, добавляемый — в ``data``)
* ``ActivityItem.JOIN_BLOG`` — вступление в блог (события выхода из блога на Табуне нет, ага)
"""
WALL_ADD = 0
POST_ADD = 1
COMMENT_ADD = 2
BLOG_ADD = 3
POST_VOTE = 11
COMMENT_VOTE = 12
BLOG_VOTE = 13
USER_VOTE = 14
FRIEND_ADD = 4
JOIN_BLOG = 24
def __init__(self, type, date, post_id=None, comment_id=None, blog=None, username=None, title=None, data=None, id=None, utctime=None):
self.type = int(type)
if self.type not in (
self.WALL_ADD, self.POST_ADD, self.COMMENT_ADD, self.BLOG_ADD,
self.POST_VOTE, self.COMMENT_VOTE, self.BLOG_VOTE,
self.USER_VOTE, self.FRIEND_ADD, self.JOIN_BLOG
):
raise ValueError
self.date = date
self.utctime = utctime
self.post_id = int(post_id) if post_id is not None else None
self.comment_id = int(comment_id) if comment_id is not None else None
self.blog = text(blog) if blog is not None else None
self.username = text(username) if username is not None else None
self.title = text(title) if title is not None else None
self.data = text(data) if data is not None else None
self.id = int(id) if id is not None else None
def __str__(self):
return "<activity " + text(self.type) + " " + (self.username or 'N/A') + ">"
def __repr__(self):
o = self.__str__()
return o.encode('utf-8') if PY2 else o
def __eq__(self, other):
return (
isinstance(other, ActivityItem) and
self.type == other.type and
self.date == other.date and
self.post_id == other.post_id and
self.comment_id == other.comment_id and
self.blog == other.blog and
self.username == other.username and
self.title == other.title and
self.data == other.data
)
def __ne__(self, other):
return not self.__eq__(other)