From 6a8bc89f3a388758ebe1c4ff61ee4719d13b000b Mon Sep 17 00:00:00 2001 From: missytake Date: Sat, 18 Mar 2023 15:45:07 +0100 Subject: [PATCH] [twitter] Deprecate Twitter backend --- .../kibicara/platforms/twitter/__init__.py | 0 backend/src/kibicara/platforms/twitter/bot.py | 164 ---------------- .../src/kibicara/platforms/twitter/model.py | 23 --- .../src/kibicara/platforms/twitter/webapi.py | 183 ------------------ 4 files changed, 370 deletions(-) delete mode 100644 backend/src/kibicara/platforms/twitter/__init__.py delete mode 100644 backend/src/kibicara/platforms/twitter/bot.py delete mode 100644 backend/src/kibicara/platforms/twitter/model.py delete mode 100644 backend/src/kibicara/platforms/twitter/webapi.py diff --git a/backend/src/kibicara/platforms/twitter/__init__.py b/backend/src/kibicara/platforms/twitter/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/kibicara/platforms/twitter/bot.py b/backend/src/kibicara/platforms/twitter/bot.py deleted file mode 100644 index dc1db68..0000000 --- a/backend/src/kibicara/platforms/twitter/bot.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (C) 2020 by Cathy Hu -# Copyright (C) 2020 by Martin Rey -# -# SPDX-License-Identifier: 0BSD - -from asyncio import CancelledError, gather, sleep -from logging import getLogger - -from peony import PeonyClient, exceptions - -from kibicara.config import config -from kibicara.platformapi import Censor, Message, Spawner -from kibicara.platforms.twitter.model import Twitter - -logger = getLogger(__name__) - - -class TwitterBot(Censor): - def __init__(self, twitter_model): - super().__init__(twitter_model.hood) - self.twitter_model = twitter_model - self.enabled = self.twitter_model.enabled - self.polling_interval_sec = 60 - self.mentions_since_id = self.twitter_model.mentions_since_id - self.dms_since_id = self.twitter_model.dms_since_id - - @classmethod - async def destroy_hood(cls, hood): - """Removes all its database entries.""" - for twitter in await Twitter.objects.filter(hood=hood).all(): - await twitter.delete() - - async def run(self): - try: - if not self.twitter_model.verified: - raise ValueError("Oauth Handshake not completed") - self.client = PeonyClient( - consumer_key=config["twitter"]["consumer_key"], - consumer_secret=config["twitter"]["consumer_secret"], - access_token=self.twitter_model.access_token, - access_token_secret=self.twitter_model.access_token_secret, - ) - if self.twitter_model.mentions_since_id is None: - logger.debug("since_id is None in model, fetch newest mention id") - await self._poll_mentions() - if self.twitter_model.dms_since_id is None: - logger.debug("since_id is None in model, fetch newest dm id") - await self._poll_direct_messages() - user = await self.client.user - if user.screen_name: - await self.twitter_model.update(username=user.screen_name) - logger.debug( - "Starting Twitter bot: {0}".format(self.twitter_model.__dict__) - ) - await gather(self.poll(), self.push()) - except CancelledError: - logger.debug( - "Bot {0} received Cancellation.".format(self.twitter_model.hood.name) - ) - except exceptions.Unauthorized: - logger.debug( - "Bot {0} has invalid auth token.".format(self.twitter_model.hood.name) - ) - await self.twitter_model.update(enabled=False) - self.enabled = self.twitter_model.enabled - except (KeyError, ValueError, exceptions.NotAuthenticated): - logger.warning("Missing consumer_keys for Twitter in your configuration.") - await self.twitter_model.update(enabled=False) - self.enabled = self.twitter_model.enabled - finally: - logger.debug("Bot {0} stopped.".format(self.twitter_model.hood.name)) - - async def poll(self): - while True: - dms = await self._poll_direct_messages() - logger.debug( - "Polled dms ({0}): {1}".format(self.twitter_model.hood.name, str(dms)) - ) - mentions = await self._poll_mentions() - logger.debug( - "Polled mentions ({0}): {1}".format( - self.twitter_model.hood.name, str(mentions) - ) - ) - await self.twitter_model.update( - dms_since_id=self.dms_since_id, mentions_since_id=self.mentions_since_id - ) - for message in dms: - await self.publish(Message(message)) - for message_id, message in mentions: - await self.publish(Message(message, twitter_mention_id=message_id)) - await sleep(self.polling_interval_sec) - - async def _poll_direct_messages(self): - dms = await self.client.api.direct_messages.events.list.get() - dms = dms.events - # TODO check for next_cursor (see twitter api) - dms_filtered = [] - if dms: - for dm in dms: - if int(dm.id) == self.dms_since_id: - break - dms_filtered.append(dm) - self.dms_since_id = int(dms[0].id) - messages = [] - for dm in dms_filtered: - filtered_text = await self._filter_text( - dm.message_create.message_data.entities, - dm.message_create.message_data.text, - ) - if not filtered_text: - continue - messages.append(filtered_text) - return messages - - async def _poll_mentions(self): - mentions = await self.client.api.statuses.mentions_timeline.get( - since_id=self.mentions_since_id - ) - if mentions: - self.mentions_since_id = mentions[0].id - messages = [] - for mention in mentions: - filtered_text = await self._filter_text(mention.entities, mention.text) - if not filtered_text: - continue - messages.append((mention.id, filtered_text)) - return messages - - async def _filter_text(self, entities, text): - remove_indices = set() - for user in entities.user_mentions: - remove_indices.update(range(user.indices[0], user.indices[1] + 1)) - for url in entities.urls: - remove_indices.update(range(url.indices[0], url.indices[1] + 1)) - for symbol in entities.symbols: - remove_indices.update(range(symbol.indices[0], symbol.indices[1] + 1)) - filtered_text = "" - for index, character in enumerate(text): - if index not in remove_indices: - filtered_text += character - return filtered_text.strip() - - async def push(self): - while True: - message = await self.receive() - logger.debug( - "Received message from censor ({0}): {1}".format( - self.twitter_model.hood.name, message.text - ) - ) - if hasattr(message, "twitter_mention_id"): - await self._retweet(message.twitter_mention_id) - else: - await self._post_tweet(message.text) - - async def _post_tweet(self, message): - return await self.client.api.statuses.update.post(status=message) - - async def _retweet(self, message_id): - return await self.client.api.statuses.retweet.post(id=message_id) - - -spawner = Spawner(Twitter, TwitterBot) diff --git a/backend/src/kibicara/platforms/twitter/model.py b/backend/src/kibicara/platforms/twitter/model.py deleted file mode 100644 index 73a76ea..0000000 --- a/backend/src/kibicara/platforms/twitter/model.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (C) 2020 by Cathy Hu -# Copyright (C) 2020 by Martin Rey -# -# SPDX-License-Identifier: 0BSD - -from ormantic import Boolean, ForeignKey, Integer, Model, Text - -from kibicara.model import Hood, Mapping - - -class Twitter(Model): - id: Integer(primary_key=True) = None - hood: ForeignKey(Hood) - dms_since_id: Integer(allow_null=True) = None - mentions_since_id: Integer(allow_null=True) = None - access_token: Text() - access_token_secret: Text() - username: Text(allow_null=True) = None - verified: Boolean() = False - enabled: Boolean() = False - - class Mapping(Mapping): - table_name = "twitterbots" diff --git a/backend/src/kibicara/platforms/twitter/webapi.py b/backend/src/kibicara/platforms/twitter/webapi.py deleted file mode 100644 index e00c467..0000000 --- a/backend/src/kibicara/platforms/twitter/webapi.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (C) 2020 by Cathy Hu -# Copyright (C) 2020 by Martin Rey -# -# SPDX-License-Identifier: 0BSD - -from logging import getLogger -from sqlite3 import IntegrityError - -from fastapi import APIRouter, Depends, HTTPException, Response, status -from ormantic.exceptions import NoMatch -from peony.exceptions import NotAuthenticated -from peony.oauth_dance import get_access_token, get_oauth_token -from pydantic import BaseModel - -from kibicara.config import config -from kibicara.platforms.twitter.bot import spawner -from kibicara.platforms.twitter.model import Twitter -from kibicara.webapi.hoods import get_hood, get_hood_unauthorized - -logger = getLogger(__name__) - - -class BodyTwitterPublic(BaseModel): - username: str - - -async def get_twitter(twitter_id: int, hood=Depends(get_hood)): - try: - return await Twitter.objects.get(id=twitter_id, hood=hood) - except NoMatch: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - - -router = APIRouter() -twitter_callback_router = APIRouter() - - -@router.get( - "/public", - # TODO response_model, - operation_id="get_twitters_public", -) -async def twitter_read_all_public(hood=Depends(get_hood_unauthorized)): - twitterbots = await Twitter.objects.filter(hood=hood).all() - return [ - BodyTwitterPublic(username=twitterbot.username) - for twitterbot in twitterbots - if twitterbot.verified == 1 and twitterbot.enabled == 1 and twitterbot.username - ] - - -@router.get( - "/", - # TODO response_model, - operation_id="get_twitters", -) -async def twitter_read_all(hood=Depends(get_hood)): - return await Twitter.objects.filter(hood=hood).all() - - -@router.get( - "/{twitter_id}", - # TODO response_model - operation_id="get_twitter", -) -async def twitter_read(twitter=Depends(get_twitter)): - return twitter - - -@router.delete( - "/{twitter_id}", - status_code=status.HTTP_204_NO_CONTENT, - # TODO response_model - operation_id="delete_twitter", -) -async def twitter_delete(twitter=Depends(get_twitter)): - spawner.stop(twitter) - await twitter.delete() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - -@router.get( - "/{twitter_id}/status", - status_code=status.HTTP_200_OK, - # TODO response_model - operation_id="status_twitter", -) -async def twitter_status(twitter=Depends(get_twitter)): - return {"status": spawner.get(twitter).status.name} - - -@router.post( - "/{twitter_id}/start", - status_code=status.HTTP_200_OK, - # TODO response_model - operation_id="start_twitter", -) -async def twitter_start(twitter=Depends(get_twitter)): - await twitter.update(enabled=True) - spawner.get(twitter).start() - return {} - - -@router.post( - "/{twitter_id}/stop", - status_code=status.HTTP_200_OK, - # TODO response_model - operation_id="stop_twitter", -) -async def twitter_stop(twitter=Depends(get_twitter)): - await twitter.update(enabled=False) - spawner.get(twitter).stop() - return {} - - -@router.post( - "/", - status_code=status.HTTP_201_CREATED, - # TODO response_model - operation_id="create_twitter", -) -async def twitter_create(response: Response, hood=Depends(get_hood)): - """ - `https://api.twitter.com/oauth/authorize?oauth_token=` - """ - try: - # Purge Twitter corpses - for corpse in await Twitter.objects.filter(hood=hood, verified=False).all(): - await corpse.delete() - # Create Twitter - request_token = await get_oauth_token( - config["twitter"]["consumer_key"], - config["twitter"]["consumer_secret"], - callback_uri="{0}/dashboard/twitter-callback?hood={1}".format( - config["frontend_url"], hood.id - ), - ) - if request_token["oauth_callback_confirmed"] != "true": - raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE) - twitter = await Twitter.objects.create( - hood=hood, - access_token=request_token["oauth_token"], - access_token_secret=request_token["oauth_token_secret"], - ) - response.headers["Location"] = str(twitter.id) - return twitter - except IntegrityError: - raise HTTPException(status_code=status.HTTP_409_CONFLICT) - except (KeyError, ValueError, NotAuthenticated): - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -@twitter_callback_router.get( - "/callback", - # TODO response_model - operation_id="callback_twitter", -) -async def twitter_read_callback( - oauth_token: str, oauth_verifier: str, hood=Depends(get_hood) -): - try: - twitter = await Twitter.objects.filter(access_token=oauth_token).get() - access_token = await get_access_token( - config["twitter"]["consumer_key"], - config["twitter"]["consumer_secret"], - twitter.access_token, - twitter.access_token_secret, - oauth_verifier, - ) - await twitter.update( - access_token=access_token["oauth_token"], - access_token_secret=access_token["oauth_token_secret"], - verified=True, - enabled=True, - ) - spawner.start(twitter) - return {} - except IntegrityError: - raise HTTPException(status_code=status.HTTP_409_CONFLICT) - except NoMatch: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - except (KeyError, ValueError, NotAuthenticated): - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)