class NGVM: def __init__(self, app_context: AppContext): super().__init__() self._app_context = app_context self._app_context.ng.on_collection_changed.add(self._ng_changed) self.on_property_changed = PropertyChangedEventHandler() @property def names(self) -> List[NGName]: return self._app_context.ng.names @property def ids(self) -> List[NGId]: return self._app_context.ng.ids @property def words(self) -> List[NGWord]: return self._app_context.ng.words @property def titles(self) -> List[NGTitle]: return self._app_context.ng.titles def update_ng(self, id, values): self._app_context.ng.update_ng(id, values) def delete_ng(self, id): self._app_context.ng.delete_ng(id) def _ng_changed(self, e: CollectionChangedEventArgs): self.on_property_changed.invoke( PropertyChangedEventArgs(self, e.property_name))
class Favorites: def __init__(self): super().__init__() self.list = [] self.on_property_changed = PropertyChangedEventHandler() self.on_order_changed = OrderChangedEventHandler() def add(self, item: Union[FavoriteThread, FavoriteBoard]): self.list.append(item) self.on_property_changed.invoke(PropertyChangedEventArgs(self, "list")) def remove(self, item: Union[FavoriteThread, FavoriteBoard]): self.list.remove(item) self.on_property_changed.invoke(PropertyChangedEventArgs(self, "list")) def raise_order(self, item: Union[FavoriteThread, FavoriteBoard]): for i, x in enumerate(self.list): if x == item: if i == 0: return else: tmp = self.list[i-1] self.list[i-1] = x self.list[i] = tmp self.on_order_changed.invoke(OrderChangedEventArg(self, "list", x, i-1)) return def lower_order(self, item: Union[FavoriteThread, FavoriteBoard]): for i, x in enumerate(self.list): if x == item: if i == len(self.list) - 1: return else: tmp = self.list[i+1] self.list[i+1] = x self.list[i] = tmp self.on_order_changed.invoke(OrderChangedEventArg(self, "list", x, i+1)) return def serialzie(self) -> str: d = {"items": []} for item in self.list: if isinstance(item, FavoriteThread): d["items"].append({"title": item.title, "server": item.server, "board": item.board, "key": item.key}) else: d["items"].append({"name": item.name, "server": item.server, "board": item.board}) return json.dumps(d) def deserialize(self, s: str): d = json.loads(s) for item in d["items"]: if "title" in item: self.list.append(FavoriteThread(item["title"], item["server"], item["board"], item["key"])) else: self.list.append(FavoriteBoard(item["name"], item["server"], item["board"]))
class ImageVM: def __init__(self, context: AppContext): super().__init__() self._app_context = context self._app_context.on_property_changed.add(self._context_changed) self.on_property_changed = PropertyChangedEventHandler() @property def image(self) -> Optional[Union[str, HTTPError, URLError]]: return self._app_context.image def _context_changed(self, e: PropertyChangedEventArgs): if e.property_name == "image": self.on_property_changed.invoke(PropertyChangedEventArgs(self, "image"))
class Bbsmenu: def __init__(self): super().__init__() self.categories = None self.dns = {} self.on_property_changed = PropertyChangedEventHandler() def update(self): html = get_bbsmenu() parser = BbsmenuParser(html) self.categories = [] for c in parser.categories(): boards = [] for b in c["boards"]: boards.append(BoardHeader(b["server"], b["board"], b["name"])) self.dns[b["board"]] = b["name"] self.categories.append(Category(c["name"], boards)) self.on_property_changed.invoke(PropertyChangedEventArgs(self, "categories"))
class FavoritesVM: def __init__(self, app_context: AppContext): super().__init__() self._app_context = app_context self._app_context.favorites.on_property_changed.add( self._context_changed) self._app_context.favorites.on_order_changed.add(self._order_changed) self.on_property_changed = PropertyChangedEventHandler() self.on_order_changed = OrderChangedEventHandler() @property def list(self): return self._app_context.favorites.list def add(self, item: Union[FavoriteThread, FavoriteBoard]): self._app_context.favorites.add(item) def remove(self, item: Union[FavoriteThread, FavoriteBoard]): self._app_context.favorites.remove(item) def raise_order(self, item: Union[FavoriteThread, FavoriteBoard]): self._app_context.favorites.raise_order(item) def lower_order(self, item: Union[FavoriteThread, FavoriteBoard]): self._app_context.favorites.lower_order(item) def open_thread(self, item: FavoriteThread): self._app_context.set_thread(item.server, item.board, item.key) def open_board(self, item: FavoriteBoard): self._app_context.set_board(item.server, item.board) def _context_changed(self, e: PropertyChangedEventArgs): self.on_property_changed.invoke(PropertyChangedEventArgs(self, "list")) def _order_changed(self, e: OrderChangedEventArg): self.on_order_changed.invoke(e)
class History: def __init__(self, max_history: int): super().__init__() self._dict = {} self._max_history = max_history self.on_property_changed = PropertyChangedEventHandler() def get(self, board: str, key: str) -> Optional[ThreadHistory]: return self._dict.get(board + key) def save(self, board: str, key: str, bookmark: int, retrieved_reses: int): while self._max_history <= len(self._dict): self._dict.popitem() self._dict[board + key] = ThreadHistory(bookmark, retrieved_reses) self.on_property_changed.invoke(PropertyChangedEventArgs(self, "self")) def serialize(self) -> str: d = {} for k, v in self._dict.items(): d[k] = { "bookmark": v.bookmark, "retrieved_reses": v.retrieved_reses } return json.dumps(d) def deserialize(self, s: str): obj = json.loads(s) for k, v in obj.items(): if len(self._dict) >= self._max_history: break self._dict[k] = ThreadHistory(v["bookmark"], v["retrieved_reses"])
class BbsmenuVM: def __init__(self, app_context: AppContext): super().__init__() self._app_context = app_context self._bbsmenu = app_context.bbsmenu self.selected_category: Optional[Category] = None self.on_property_changed = PropertyChangedEventHandler() self._app_context.on_property_changed.add(self._app_context_changed) @property def categories(self) -> Optional[List[Category]]: return self._bbsmenu.categories if self._bbsmenu is not None else None def select_category(self, idx: int): if self._bbsmenu is not None and idx < len(self._bbsmenu.categories)\ and idx >= 0: self.selected_category = self._bbsmenu.categories[idx] self.on_property_changed.invoke( PropertyChangedEventArgs(self, "selected_category")) def select_board(self, idx: int): if self.selected_category is not None and idx < len( self.selected_category.boards) and idx >= 0: self._app_context.set_board( self.selected_category.boards[idx].server, self.selected_category.boards[idx].board) def update(self): if self._bbsmenu is not None: self._bbsmenu.update() def _app_context_changed(self, e: PropertyChangedEventArgs): if e.property_name == "bbsmenu": if self._bbsmenu is not None: self._bbsmenu.on_property_changed.remove(self._bbsmenu_changed) self._bbsmenu = self._app_context.bbsmenu self._bbsmenu.on_property_changed.add(self._bbsmenu_changed) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "categories")) def _bbsmenu_changed(self, e: PropertyChangedEventArgs): if e.property_name == "categories": self.on_property_changed.invoke( PropertyChangedEventArgs(self, "categories"))
class BoardVM: def __init__(self, app_context: AppContext): super().__init__() self._app_context = app_context self._board = app_context.board self._threads = None if "number" in DEFAULT_SORT: self._sort_by = "number" elif "title" in DEFAULT_SORT: self._sort_by = "title" elif "count" in DEFAULT_SORT: self._sort_by = "count" elif "speed" in DEFAULT_SORT: self._sort_by = "speed" self._reverse_sort = DEFAULT_SORT.startswith("!") self._active_sort = False self._search_word = None self._app_context.on_property_changed.add(self._app_context_changed) self._app_context.ng.on_collection_changed.add(self._ng_changed) self._app_context.history.on_property_changed.add( self._history_changed) self._app_context.favorites.on_property_changed.add( self.favorite_changed) self.on_property_changed = PropertyChangedEventHandler() @property def board(self) -> Optional[str]: return self._board.board if self._board is not None else None @property def threads(self) -> Optional[List[ThreadHeader]]: return self._threads @property def name(self) -> Optional[str]: return self._app_context.bbsmenu.dns[self._board.board] \ if self._board is not None else None @property def is_favorite(self) -> Optional[bool]: if self._board is not None: if self._board.board in [ x.board for x in self._app_context.favorites.list if isinstance(x, FavoriteBoard) ]: return True else: return False def sort_threads(self, sort_by: str, reverse_sort: bool = False): self._sort_by = sort_by self._reverse_sort = reverse_sort self._search_word = None self._update_threads() def sort_threads_by_word(self, word: str): self._search_word = word self._update_threads() def switch_active_sort(self): self._active_sort = not self._active_sort self._update_threads() def set_thread(self, header: ThreadHeader): self._app_context.set_thread(self._board.server, self._board.board, header.key) def update(self): if self._board is not None: self._board.update() def add_ng_title(self, value, use_reg, board): self._app_context.ng.add_ng_title(value, use_reg, board) def favorite(self): if self._app_context.board is not None: target = self._app_context.board # Check if the board has already been registered for f in self._app_context.favorites.list: if isinstance(f, FavoriteBoard) and f.board == target.board: self._app_context.favorites.remove(f) return board_name = self._app_context.bbsmenu.dns[target.board] self._app_context.favorites.add( FavoriteBoard(board_name, target.server, target.board)) def _update_threads(self): if self._board is None: return self._threads = [] for t in self._app_context.ng.filter_threads(self._board.threads, self._board.board): history = self._app_context.history.get(self._board.board, t.key) if history is not None: unread = max(t.count, history.retrieved_reses) - history.bookmark self._threads.append( ThreadHeaderVM(t.key, t.number, t.title, max(t.count, history.retrieved_reses), t.is_new, t.speed, unread)) else: self._threads.append( ThreadHeaderVM(t.key, t.number, t.title, t.count, t.is_new, t.speed, None)) if self._sort_by == "number": self._threads.sort(key=lambda x: x.number, reverse=self._reverse_sort) elif self._sort_by == "title": self._threads.sort(key=lambda x: x.title, reverse=self._reverse_sort) elif self._sort_by == "count": self._threads.sort(key=lambda x: x.count, reverse=self._reverse_sort) elif self._sort_by == "speed": self._threads.sort(key=lambda x: x.speed, reverse=self._reverse_sort) if self._active_sort: self._threads.sort(key=self._active_sort_key, reverse=True) if self._search_word is not None: self._threads.sort( key=lambda x: (self._search_word not in x.title)) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "threads")) def _app_context_changed(self, e: PropertyChangedEventArgs): if e.property_name == "board": if self._board is not None: self._board.on_property_changed.remove(self._board_changed) self._board = self._app_context.board self._board.on_property_changed.add(self._board_changed) self._update_threads() def _board_changed(self, e: PropertyChangedEventArgs): if e.property_name == "threads": self._search_word = None self._update_threads() def _ng_changed(self, e: CollectionChangedEventArgs): self._search_word = None self._update_threads() def _history_changed(self, e: PropertyChangedEventArgs): self._update_threads() def favorite_changed(self, e: PropertyChangedEventArgs): self.on_property_changed.invoke( PropertyChangedEventArgs(self, "is_favorite")) def _active_sort_key(self, item: ThreadHeaderVM): if item.unread is None: if item.is_new: return 1 else: return 0 else: if item.unread == 0: return 2 else: return 3
class AppContext: def __init__(self): super().__init__() self.bbsmenu: Optional[Bbsmenu] = None self.board: Optional[Board] = None self.thread: Optional[Thread] = None self.image: Optional[Union[str, HTTPError, URLError]] = None self.ng: NG = NG() if NG_PATH.is_file(): s = NG_PATH.read_text() # Ensure it's not empty if len(s.replace(" ", "")) != 0: self.ng.deserialize(s) self.history = History(MAX_HISTORY) if HISTORY_PATH.is_file(): s = HISTORY_PATH.read_text() self.history.deserialize(s) self.favorites = Favorites() if FAVORITES_PATH.is_file(): s = FAVORITES_PATH.read_text() self.favorites.deserialize(s) self.on_property_changed = PropertyChangedEventHandler() def set_bbsmenu(self): self.bbsmenu = Bbsmenu() self.bbsmenu.update() self.on_property_changed.invoke( PropertyChangedEventArgs(self, "bbsmenu")) def set_board(self, server: str, board: str): if CACHE_BOARD: self.save_board() if board_cache.contains(board): s = board_cache.get(board) self.board = Board.deserialize(s) self.board.update() self.on_property_changed.invoke( PropertyChangedEventArgs(self, "board")) return self.board = Board(server, board) self.board.update() self.on_property_changed.invoke(PropertyChangedEventArgs( self, "board")) def set_thread(self, server: str, board: str, key: str): if CACHE_THREAD: self.save_thread() if thread_cache.contains(board + key): s = thread_cache.get(board + key) self.thread = Thread.deserialize(s) self.thread.update() self.on_property_changed.invoke( PropertyChangedEventArgs(self, "thread")) return self.thread = Thread(server, board, key) self.thread.init() self.on_property_changed.invoke( PropertyChangedEventArgs(self, "thread")) def save_board(self): if self.board is not None: s = self.board.serialize() board_cache.store(self.board.board, s.encode()) def save_thread(self): if self.thread is not None: s = self.thread.serialize() thread_cache.store(self.thread.board + self.thread.key, s.encode()) def save_context(self): if CACHE_BOARD: self.save_board() if CACHE_THREAD: self.save_thread() s = self.ng.serialize() NG_PATH.write_text(s) s = self.history.serialize() HISTORY_PATH.write_text(s) s = self.favorites.serialzie() FAVORITES_PATH.write_text(s) def set_image(self, url: str): if CACHE_IMAGE: file_name = re.sub(r'https?://|/', "", url) if image_cache.contains(file_name): self.image = image_cache.path + "/" + file_name else: result = download_image(url) if isinstance(result, HTTPError) or isinstance( result, URLError): self.image = result else: image_cache.store(file_name, result) self.image = image_cache.path + "/" + file_name else: result = download_image(url) if isinstance(result, HTTPError) or isinstance(result, URLError): self.image = result else: f = tempfile.NamedTemporaryFile() f.write(result) self.image = f.name f.close() self.on_property_changed.invoke(PropertyChangedEventArgs( self, "image"))
class ThreadVM: def __init__(self, app_context: AppContext): super().__init__() self._app_context = app_context self._thread = app_context.thread self._responses = None self._links = None self._replies = None self._ids = None self.on_property_changed = PropertyChangedEventHandler() self.on_collection_changed = CollectionChangedEventHandler() app_context.on_property_changed.add(self._app_context_changed) app_context.ng.on_collection_changed.add(self._ng_changed) app_context.history.on_property_changed.add(self._history_changed) app_context.favorites.on_property_changed.add(self._favorites_changed) @property def server(self) -> Optional[str]: return self._thread.server if self._thread is not None else None @property def board(self) -> Optional[str]: return self._thread.board if self._thread is not None else None @property def key(self) -> Optional[str]: return self._thread.key if self._thread is not None else None @property def title(self) -> Optional[str]: return self._thread.title if self._thread is not None else None @property def is_pastlog(self) -> Optional[bool]: return self._thread.is_pastlog if self._thread is not None else None @property def responses(self) -> Optional[List[Union[Response, Aborn, Hide]]]: return self._responses @property def links(self) -> Optional[List[str]]: return self._links @property def replies(self) -> Optional[List[str]]: return self._replies @property def ids(self) -> Optional[Dict[str, List[Response]]]: return self._ids @property def bookmark(self) -> Optional[int]: if self._thread is not None: history = self._app_context.history.get(self._thread.board, self._thread.key) return history.bookmark if history is not None else None @property def is_favorite(self) -> Optional[bool]: if self._thread is not None: if self._thread.key in [ x.key for x in self._app_context.favorites.list if isinstance(x, FavoriteThread) ]: return True else: return False @property def ng(self) -> NG: return self._app_context.ng def update(self): if self._app_context.thread is not None: self._app_context.thread.update() def set_thread(self, server: str, board: str, key: str): self._app_context.set_thread(server, board, key) def set_image(self, url: str): self._app_context.set_image(url) def post(self, name: str, mail: str, msg: str) -> Optional[str]: if self._thread is not None: return self._thread.post(name, mail, msg) return None def add_ng_name(self, value, use_reg, hide, auto_ng_id, board, key): self._app_context.ng.add_ng_name(value, use_reg, hide, auto_ng_id, board, key) def add_ng_id(self, value, use_reg, hide, board, key): self._app_context.ng.add_ng_id(value, use_reg, hide, board, key) def add_ng_word(self, value, use_reg, hide, auto_ng_id, board, key): self._app_context.ng.add_ng_word(value, use_reg, hide, auto_ng_id, board, key) def save_history(self, bookmark: int): self._app_context.history.save(self._thread.board, self._thread.key, bookmark, len(self._thread.responses)) def favorite(self): if self._app_context.thread is not None: target = self._app_context.thread # Check if the thread has already been registered for f in self._app_context.favorites.list: if isinstance( f, FavoriteThread ) and f.key == target.key and f.board == target.board: self._app_context.favorites.remove(f) return self._app_context.favorites.add( FavoriteThread(self._app_context.thread.title, self._app_context.thread.server, self._app_context.thread.board, self._app_context.thread.key)) def _app_context_changed(self, e: PropertyChangedEventArgs): if e.property_name == "thread": if self._thread is not None: self._thread.on_property_changed.remove( self._thread_property_changed) self._thread.on_collection_changed.remove( self._thread_collection_changed) self._thread = self._app_context.thread self._thread.on_property_changed.add(self._thread_property_changed) self._thread.on_collection_changed.add( self._thread_collection_changed) self._responses = [] self._links = [] self._replies = {} self._ids = {} for r in self._thread.responses: self._filter_response(r) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "server")) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "board")) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "key")) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "title")) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "is_pastlog")) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "responses")) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "links")) def _thread_property_changed(self, e: PropertyChangedEventArgs): if e.property_name == "is_pastlog": self.on_property_changed.invoke( PropertyChangedEventArgs(self, "is_pastlog")) def _thread_collection_changed(self, e: CollectionChangedEventArgs): if e.property_name == "responses": last_count = len(self._responses) for r in e.item: self._filter_response(r) self.on_collection_changed.invoke( CollectionChangedEventArgs(self, "responses", CollectionChangedEventKind.EXTEND, self._responses[last_count:])) def _ng_changed(self, e: CollectionChangedEventArgs): if self._thread is not None: self._responses = [] self._links = [] self._replies = {} self._ids = {} for r in self._thread.responses: self._filter_response(r) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "responses")) self.on_property_changed.invoke(PropertyChangedEventArgs(self, "ng")) def _history_changed(self, e: PropertyChangedEventArgs): self.on_property_changed.invoke( PropertyChangedEventArgs(self, "history")) def _favorites_changed(self, e: PropertyChangedEventArgs): self.on_property_changed.invoke( PropertyChangedEventArgs(self, "is_favorite")) def _filter_response(self, r: Response): """ Before calling this method, make sure self._thread is not None & self._responses, self._links, self._replies and self._ids have been initialized """ kind = self._app_context.ng.is_ng_response(r, self._thread.board, self._thread.key) if kind == NGKind.NOT_NG: self._responses.append(r) for link in re.finditer(r'(https?://.*?)(?=$|\n| )', r.message): self._links.append(link.group(1)) for reply in re.finditer(r'>>(\d{1,4})', r.message): key = int(reply.group(1)) if key not in self._replies: self._replies[key] = [] if r not in self._replies[key]: self._replies[key].append(r) if r.id in self._ids: self._ids[r.id].append(r) else: self._ids[r.id] = [r] elif kind == NGKind.HIDE: self._responses.append(Hide(r)) else: self._responses.append(Aborn(r))
class Board: def __init__(self, server: str, board: str): super().__init__() self.server = server self.board = board self.threads: List[ThreadHeader] = [] self.on_property_changed = PropertyChangedEventHandler() def update(self): s = get_board(self.server, self.board) parser = BoardParser(s) new_threads = [] for i, t in enumerate(parser.threads(), 1): # If thread is new arrival if t["key"] not in [x.key for x in self.threads]: now = int(time.time()) key = int(t["key"]) # If the time since thread is created is less than NEW_THREAD_INTERVAL minutes from now if now - (NEW_THREAD_INTERVAL * 60) < key: new_threads.append( ThreadHeader(t["key"], i, t["title"], t["count"], True)) continue new_threads.append( ThreadHeader(t["key"], i, t["title"], t["count"], False)) self.threads = new_threads self.on_property_changed.invoke( PropertyChangedEventArgs(self, "threads")) def sort_threads(self, key: str, reverse=False): if key == "number": self.threads.sort(key=lambda x: x.number, reverse=reverse) elif key == "title": self.threads.sort(key=lambda x: x.title, reverse=reverse) elif key == "count": self.threads.sort(key=lambda x: x.count, reverse=reverse) elif key == "unread": self.threads.sort(key=lambda x: x.count - x.bookmark if x.bookmark != 0 else -1, reverse=True) elif key == "speed": self.threads.sort(key=lambda x: x.speed, reverse=reverse) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "threads")) def sort_threads_by_word(self, word: str): self.threads.sort(key=lambda x: (word not in x.title)) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "threads")) def serialize(self) -> str: d = {} d["server"] = self.server d["board"] = self.board d["threads"] = [] for t in self.threads: d["threads"].append({ "number": t.number, "title": t.title, "count": t.count, "speed": t.speed, "key": t.key, "is_new": t.is_new }) return json.dumps(d) @staticmethod def deserialize(s: str): obj = json.loads(s) b = Board(obj["server"], obj["board"]) for t in obj["threads"]: b.threads.append( ThreadHeader(t["key"], t["number"], t["title"], t["count"], t["is_new"])) return b def _thread_property_changed(self, e: PropertyChangedEventArgs): if e.property_name == "bookmark": self.on_property_changed.invoke( PropertyChangedEventArgs(self, "threads"))
class Thread: def __init__(self, server: str, board: str, key: str): super().__init__() self.server = server self.board = board self.key = key self.title = None self.responses: List[Response] = [] self._is_pastlog: bool = False self.on_property_changed = PropertyChangedEventHandler() self.on_collection_changed = CollectionChangedEventHandler() @property def is_pastlog(self) -> bool: return self._is_pastlog @is_pastlog.setter def is_pastlog(self, value: bool): self._is_pastlog = value self.on_property_changed.invoke( PropertyChangedEventArgs(self, "is_pastlog")) def serialize(self) -> str: d = {} d["server"] = self.server d["board"] = self.board d["key"] = self.key d["title"] = self.title d["is_pastlog"] = self.is_pastlog d["responses"] = [] for r in self.responses: d2 = {} d2["number"] = r.number d2["name"] = r.name d2["mail"] = r.mail d2["date"] = r.date d2["id"] = r.id d2["message"] = r.message d["responses"].append(d2) return json.dumps(d, ensure_ascii=False) @staticmethod def deserialize(s: str) -> "Thread": d = json.loads(s) t = Thread(d["server"], d["board"], d["key"]) t.title = d["title"] t.is_pastlog = d["is_pastlog"] for r in d["responses"]: t.responses.append( Response(r["number"], r["name"], r["mail"], r["date"], r["id"], r["message"])) return t def init(self): html = get_responses_after(self.server, self.board, self.key, len(self.responses)) parser = ThreadParserH(html) self.is_pastlog = parser.is_pastlog() if self.title is None: self.title = parser.title() for r in parser.responses(): self.responses.append( Response(r["number"], r["name"], r["mail"], r["date"], r["id"], r["message"])) self.on_property_changed.invoke( PropertyChangedEventArgs(self, "is_pastlog")) self.on_property_changed.invoke(PropertyChangedEventArgs( self, "title")) self.on_collection_changed.invoke( CollectionChangedEventArgs(self, "responses", CollectionChangedEventKind.EXTEND, self.responses[0:])) def update(self): html = get_responses_after(self.server, self.board, self.key, len(self.responses)) parser = ThreadParserH(html) self.is_pastlog = parser.is_pastlog() self.on_property_changed.invoke( PropertyChangedEventArgs(self, "is_pastlog")) new_responses = [] for r in parser.responses(): new_responses.append( Response(r["number"], r["name"], r["mail"], r["date"], r["id"], r["message"])) last_count = len(self.responses) # There are no new posts if len(new_responses) == 1: return self.responses.extend(new_responses[1:]) self.on_collection_changed.invoke( CollectionChangedEventArgs(self, "responses", CollectionChangedEventKind.EXTEND, self.responses[last_count:])) def post(self, name: str, mail: str, message: str) -> str: return post_response(self.server, self.board, self.key, name, mail, message)