-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathdms.py
177 lines (141 loc) Β· 6.67 KB
/
dms.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Protocol-independent code for sending and receiving DMs aka chat messages."""
from datetime import timedelta
import logging
from granary import as1
from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil import util
from common import create_task, memcache, memcache_key
import ids
import models
import protocol
logger = logging.getLogger(__name__)
REQUESTS_LIMIT_EXPIRE = timedelta(days=1)
REQUESTS_LIMIT_USER = 10
def maybe_send(*, from_proto, to_user, text, type=None):
"""Sends a DM.
Creates a task to send the DM asynchronously.
If ``type`` is provided, and we've already sent this user a DM of this type
from this protocol, does nothing.
Args:
from_proto (protocol.Protocol)
to_user (models.User)
text (str): message content. May be HTML.
type (str): optional, one of DM.TYPES
"""
if type:
dm = models.DM(protocol=from_proto.LABEL, type=type)
if dm in to_user.sent_dms:
return
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
id = f'{bot.profile_id()}#{type or "?"}-dm-{to_user.key.id()}-{util.now().isoformat()}'
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()
create_task(queue='send', obj=obj_key.urlsafe(), protocol=to_user.LABEL,
url=target.uri, user=bot.key.urlsafe())
if type:
to_user.sent_dms.append(dm)
to_user.put()
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")}')
# remove @-mentions in HTML links
soup = util.parse_html(inner_obj.get('content', ''))
for link in soup.find_all('a'):
link.extract()
content = soup.get_text().strip().lower()
# parse and handle message
if content in ('yes', 'ok'):
from_user.enable_protocol(to_proto)
to_proto.bot_follow(from_user)
return 'OK', 200
elif content == 'no':
from_user.delete(to_proto)
from_user.disable_protocol(to_proto)
return 'OK', 200
# are they requesting a user?
if not to_proto.owns_handle(content) and content.startswith('@'):
logging.info("doesn't look like a handle, trying without leading @")
content = content.removeprefix('@')
if to_proto.owns_handle(content) is not False:
handle = content
from_proto = from_user.__class__
def reply(text, type=None):
maybe_send(from_proto=to_proto, to_user=from_user, text=text, type=type)
return 'OK', 200
try:
ids.translate_handle(handle=handle, from_=to_proto, to=from_user,
enhanced=False)
except ValueError as e:
logger.warning(e)
return reply(f"Sorry, Bridgy Fed doesn't yet support bridging handle {handle} from {to_proto.PHRASE} to {from_proto.PHRASE}.")
to_id = to_proto.handle_to_id(handle)
if not to_id:
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")
if not from_user.is_enabled(to_proto):
return reply(f'Please bridge your account to {to_proto.PHRASE} by following this account before requesting another user.')
to_user = to_proto.get_or_create(to_id)
if not to_user:
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")
if not to_user.obj:
# doesn't exist
return reply(f"Couldn't find {to_proto.PHRASE} user {handle}")
elif to_user.is_enabled(from_proto):
# already bridged
return reply(f'{to_user.user_link(proto=from_proto)} is already bridged into {from_proto.PHRASE}.')
elif (models.DM(protocol=from_proto.LABEL, type='request_bridging')
in to_user.sent_dms):
# already requested
return reply(f"We've already sent {to_user.user_link()} a DM. Fingers crossed!")
# 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,
expire=int(REQUESTS_LIMIT_EXPIRE.total_seconds()))
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!
maybe_send(from_proto=from_proto, to_user=to_user, type='request_bridging', text=f"""\
<p>Hi! {from_user.user_link(proto=to_proto, proto_fallback=True)} 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.
<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.
<p>Bridgy Fed will only send you this message once.""")
return reply(f"Got it! We'll send {to_user.user_link()} a message and say that you hope they'll enable the bridge. Fingers crossed!")
error(f"Couldn't understand DM: {content}", status=304)