[twitter] Deprecate Twitter backend
This commit is contained in:
parent
64715f5aa5
commit
6a8bc89f3a
|
@ -1,164 +0,0 @@
|
|||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||
#
|
||||
# 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)
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||
#
|
||||
# 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"
|
|
@ -1,183 +0,0 @@
|
|||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||
#
|
||||
# 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)
|
Loading…
Reference in a new issue