2024-08-18 05:53:14 +00:00
""" Protocol-independent code for sending and receiving DMs aka chat messages. """
2024-08-23 00:12:56 +00:00
from datetime import timedelta
2024-08-18 05:53:14 +00:00
import logging
2024-08-18 15:28:11 +00:00
from granary import as1
2024-08-18 05:53:14 +00:00
from oauth_dropins . webutil import util
2024-08-23 00:12:56 +00:00
from common import create_task , memcache , memcache_key
2024-08-18 05:53:14 +00:00
import models
2024-08-18 15:28:11 +00:00
import protocol
2024-08-18 05:53:14 +00:00
logger = logging . getLogger ( __name__ )
2024-08-23 00:12:56 +00:00
REQUESTS_LIMIT_EXPIRE = timedelta ( days = 1 )
REQUESTS_LIMIT_USER = 10
2024-08-18 05:53:14 +00:00
2024-08-19 20:55:05 +00:00
def maybe_send ( * , from_proto , to_user , text , type = None ) :
""" Sends a DM.
2024-08-18 05:53:14 +00:00
Creates a task to send the DM asynchronously .
2024-08-19 20:55:05 +00:00
If ` ` type ` ` is provided , and we ' ve already sent this user a DM of this type
from this protocol , does nothing .
2024-08-18 05:53:14 +00:00
Args :
from_proto ( protocol . Protocol )
to_user ( models . User )
text ( str ) : message content . May be HTML .
2024-08-19 20:55:05 +00:00
type ( str ) : optional , one of DM . TYPES
2024-08-18 05:53:14 +00:00
"""
2024-08-19 20:55:05 +00:00
if type :
dm = models . DM ( protocol = from_proto . LABEL , type = type )
if dm in to_user . sent_dms :
return
2024-08-18 05:53:14 +00:00
from web import Web
bot = Web . get_by_id ( from_proto . bot_user_id ( ) )
logger . info ( f ' Sending DM from { bot . key . id ( ) } to { to_user . key . id ( ) } : { text } ' )
if not to_user . obj or not to_user . obj . as1 :
logger . info ( " can ' t send DM, recipient has no profile obj " )
return
2024-08-19 20:55:05 +00:00
id = f ' { bot . profile_id ( ) } # { type or " ? " } -dm- { to_user . key . id ( ) } - { util . now ( ) . isoformat ( ) } '
2024-08-18 05:53:14 +00:00
target_uri = to_user . target_for ( to_user . obj , shared = False )
target = models . Target ( protocol = to_user . LABEL , uri = target_uri )
obj_key = models . Object ( id = id , source_protocol = ' web ' , undelivered = [ target ] ,
our_as1 = {
' objectType ' : ' activity ' ,
' verb ' : ' post ' ,
' id ' : f ' { id } -create ' ,
' actor ' : bot . key . id ( ) ,
' object ' : {
' objectType ' : ' note ' ,
' id ' : id ,
' author ' : bot . key . id ( ) ,
' content ' : text ,
' tags ' : [ {
' objectType ' : ' mention ' ,
' url ' : to_user . key . id ( ) ,
} ] ,
' to ' : [ to_user . key . id ( ) ] ,
} ,
' to ' : [ to_user . key . id ( ) ] ,
} ) . put ( )
2024-08-23 00:12:56 +00:00
create_task ( queue = ' send ' , obj = obj_key . urlsafe ( ) , protocol = to_user . LABEL ,
url = target . uri , user = bot . key . urlsafe ( ) )
2024-08-18 05:53:14 +00:00
2024-08-19 20:55:05 +00:00
if type :
to_user . sent_dms . append ( dm )
to_user . put ( )
2024-08-18 05:53:14 +00:00
2024-08-18 15:28:11 +00:00
def receive ( * , from_user , obj ) :
""" Handles a DM that a user sent to one of our protocol bot users.
Args :
from_user ( models . User )
obj ( Object ) : DM
Returns :
( str , int ) tuple : ( response body , HTTP status code ) Flask response
"""
recip = as1 . recipient_if_dm ( obj . as1 )
assert recip
to_proto = protocol . Protocol . for_bridgy_subdomain ( recip )
assert to_proto # already checked in check_supported call in Protocol.receive
inner_obj = ( as1 . get_object ( obj . as1 ) if as1 . object_type ( obj . as1 ) == ' post '
else obj . as1 )
logger . info ( f ' got DM from { from_user . key . id ( ) } to { to_proto . LABEL } : { inner_obj . get ( " content " ) } ' )
2024-08-19 20:26:45 +00:00
# remove @-mentions in HTML links
2024-08-18 15:28:11 +00:00
soup = util . parse_html ( inner_obj . get ( ' content ' , ' ' ) )
for link in soup . find_all ( ' a ' ) :
link . extract ( )
content = soup . get_text ( ) . strip ( ) . lower ( )
2024-08-19 20:26:45 +00:00
# parse and handle message
2024-08-18 15:28:11 +00:00
if content in ( ' yes ' , ' ok ' ) :
from_user . enable_protocol ( to_proto )
to_proto . bot_follow ( from_user )
2024-08-19 20:26:45 +00:00
return ' OK ' , 200
2024-08-18 15:28:11 +00:00
elif content == ' no ' :
to_proto . delete_user_copy ( from_user )
from_user . disable_protocol ( to_proto )
2024-08-19 20:26:45 +00:00
return ' OK ' , 200
2024-08-20 03:36:07 +00:00
# request a user
2024-08-19 20:26:45 +00:00
elif to_proto . owns_handle ( content ) is not False :
2024-08-20 04:43:35 +00:00
def reply ( text , type = None ) :
2024-08-20 03:36:07 +00:00
maybe_send ( from_proto = to_proto , to_user = from_user , text = text , type = type )
2024-08-19 21:05:01 +00:00
return ' OK ' , 200
2024-08-20 03:36:07 +00:00
if not from_user . is_enabled ( to_proto ) :
2024-08-20 04:43:35 +00:00
return reply ( f ' Please bridge your account to { to_proto . PHRASE } by following this account before requesting another user. ' )
2024-08-20 03:36:07 +00:00
2024-08-19 20:26:45 +00:00
if to_id := to_proto . handle_to_id ( content ) :
2024-08-20 03:36:07 +00:00
handle = content
2024-08-19 20:26:45 +00:00
if to_user := to_proto . get_or_create ( to_id ) :
2024-08-20 03:36:07 +00:00
from_proto = from_user . __class__
if not to_user . obj :
# doesn't exist
2024-08-20 04:43:35 +00:00
return reply ( f " Couldn ' t find { to_proto . PHRASE } user { handle } " )
2024-08-20 03:36:07 +00:00
elif to_user . is_enabled ( from_proto ) :
# already bridged
2024-08-21 04:37:32 +00:00
return reply ( f ' { to_user . user_link ( ) } is already bridged into { from_proto . PHRASE } . ' )
2024-08-20 03:36:07 +00:00
elif ( models . DM ( protocol = from_proto . LABEL , type = ' request_bridging ' )
in to_user . sent_dms ) :
# already requested
2024-08-21 04:37:32 +00:00
return reply ( f " We ' ve already sent { to_user . user_link ( ) } a DM. Fingers crossed! " )
2024-08-20 03:36:07 +00:00
2024-08-23 00:12:56 +00:00
# check and update rate limits
attempts_key = f ' dm-user-requests- { from_user . LABEL } - { from_user . key . id ( ) } '
# incr leaves existing expiration as is, doesn't change it
# https://stackoverflow.com/a/4084043/186123
attempts = memcache . incr ( attempts_key , 1 )
if not attempts :
memcache . add ( attempts_key , 1 ,
2024-08-23 00:57:37 +00:00
expire = int ( REQUESTS_LIMIT_EXPIRE . total_seconds ( ) ) )
2024-08-23 00:12:56 +00:00
elif attempts > REQUESTS_LIMIT_USER :
return reply ( f " Sorry, you ' ve hit your limit of { REQUESTS_LIMIT_USER } requests per day. Try again tomorrow! " )
# send the DM request!
2024-08-20 03:36:07 +00:00
maybe_send ( from_proto = from_proto , to_user = to_user ,
2024-08-19 20:26:45 +00:00
type = ' request_bridging ' , text = f """ \
2024-08-25 20:38:11 +00:00
< p > Hi ! { from_user . user_link ( proto = to_proto ) } is using Bridgy Fed to bridge their account from { from_proto . PHRASE } into { to_proto . PHRASE } , and they ' d like to follow you. You can bridge your account into {from_proto.PHRASE} by following this account. <a href= " https://fed.brid.gy/docs " >See the docs</a> for more information.
2024-08-20 03:36:07 +00:00
< p > If you do nothing , your account won ' t be bridged, and users on {from_proto.PHRASE} won ' t be able to see or interact with you .
2024-08-19 20:26:45 +00:00
< p > Bridgy Fed will only send you this message once . """ )
2024-08-21 04:37:32 +00:00
return reply ( f " Got it! We ' ll send { to_user . user_link ( ) } a DM. Fingers crossed! " )
2024-08-18 15:28:11 +00:00
2024-08-23 00:12:56 +00:00
return reply ( f " Couldn ' t find { to_proto . PHRASE } user { handle } " )
2024-08-19 21:05:01 +00:00
return " Couldn ' t understand DM: foo bar " , 304