[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