def undo_follow(self, undo_unwrapped): """Replies to an AP Follow request with an Accept request. Args: undo_unwrapped: dict, AP Undo activity with redirect URLs unwrapped """ logging.info('Undoing Follow') follow = undo_unwrapped.get('object', {}) follower = follow.get('actor') followee = follow.get('object') if not follower or not followee: self.error( 'Undo of Follow requires object with actor and object. Got: %s' % follow) # deactivate Follower user_domain = util.domain_from_link(followee) follower_obj = Follower.get_by_id(Follower._id(user_domain, follower)) if follower_obj: logging.info('Marking %s as inactive' % follower_obj.key) follower_obj.status = 'inactive' follower_obj.put() else: logging.warning('No Follower found for %s %s', user_domain, follower)
def test_inbox_undo_follow_inactive(self, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='https://realize.be/') Follower(id=Follower._id('realize.be', 'https://mastodon.social/users/swentel'), status='inactive').put() got = self.client.post('/foo.com/inbox', json=UNDO_FOLLOW_WRAPPED) self.assertEqual(200, got.status_code)
def test_inbox_delete_actor(self, mock_head, mock_get, mock_post): follower = Follower.get_or_create('realize.be', DELETE['actor']) Follower.get_or_create('snarfed.org', DELETE['actor']) # other unrelated follower other = Follower.get_or_create('realize.be', 'https://mas.to/users/other') self.assertEqual(3, Follower.query().count()) got = self.client.post('/realize.be/inbox', json=DELETE) self.assertEqual(200, got.status_code)
def accept_follow(self, follow, follow_unwrapped): """Replies to an AP Follow request with an Accept request. Args: follow: dict, AP Follow activity follow_unwrapped: dict, same, except with redirect URLs unwrapped """ logging.info('Replying to Follow with Accept') followee = follow.get('object') followee_unwrapped = follow_unwrapped.get('object') follower = follow.get('actor') if not followee or not followee_unwrapped or not follower: common.error( self, 'Follow activity requires object and actor. Got: %s' % follow) inbox = follower.get('inbox') follower_id = follower.get('id') if not inbox or not follower_id: common.error(self, 'Follow actor requires id and inbox. Got: %s', follower) # store Follower user_domain = util.domain_from_link(followee_unwrapped) Follower.get_or_create(user_domain, follower_id, last_follow=json.dumps(follow)) # send AP Accept accept = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': util.tag_uri(appengine_config.HOST, 'accept/%s/%s' % ((user_domain, follow.get('id')))), 'type': 'Accept', 'actor': followee, 'object': { 'type': 'Follow', 'actor': follower_id, 'object': followee, } } resp = send(accept, inbox, user_domain) self.response.status_int = resp.status_code self.response.write(resp.text) # send webmention common.send_webmentions(self, as2.to_as1(follow), proxy=True, protocol='activitypub', source_as2=json.dumps(follow_unwrapped))
def test_inbox_undo_follow(self, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='https://realize.be/') Follower(id=Follower._id('realize.be', FOLLOW['actor'])).put() got = self.client.post('/foo.com/inbox', json=UNDO_FOLLOW_WRAPPED) self.assertEqual(200, got.status_code) follower = Follower.get_by_id('realize.be %s' % FOLLOW['actor']) self.assertEqual('inactive', follower.status)
def test_inbox_undo_follow_inactive(self, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='https://realize.be/') Follower(id=Follower._id('realize.be', 'https://mastodon.social/users/swentel'), status='inactive').put() got = application.get_response( '/foo.com/inbox', method='POST', body=json_dumps(UNDO_FOLLOW_WRAPPED).encode()) self.assertEqual(200, got.status_int)
def test_inbox_undo_follow(self, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='https://realize.be/') Follower(id=Follower._id('realize.be', FOLLOW['actor'])).put() got = application.get_response( '/foo.com/inbox', method='POST', body=json_dumps(UNDO_FOLLOW_WRAPPED).encode()) self.assertEqual(200, got.status_int) follower = Follower.get_by_id('realize.be %s' % FOLLOW['actor']) self.assertEqual('inactive', follower.status)
def change_follow(request, format=None): if request.method == 'POST': followForm = ChangeFollowForm(request.POST) if followForm.is_valid(): username = followForm.cleaned_data['username'] password = followForm.cleaned_data['password'] target_user = followForm.cleaned_data['target_user'] if username == target_user: return Response({'resMsg': '不能关注自己'}) s_type = followForm.cleaned_data['s_type'] user = User.objects.filter(username__exact=username, password__exact=password) target = User.objects.filter(username__exact=target_user) if user: if target: serializer_user = UserSerializer(user[0]) pk_user = serializer_user.data['id'] serializer_target = UserSerializer(target[0]) pk_target = serializer_target.data['id'] # 针对自己的关注 if s_type == TYPE_FOLLOW: # 如果有关注就取关,没有就关注,相当于异或运算 f = Follower.objects.filter(u1_id__exact=pk_target, u2_id__exact=pk_user) if f: f.delete() return Response({'resMsg': '取消关注成功'}) else: new_f = Follower(u1_id=pk_target, u2_id=pk_user) new_f.save() return Response({'resMsg': '关注成功'}) # 针对自己的粉丝 elif s_type == TYPE_FOLLOWING: # 移除粉丝 f = Follower.objects.filter(u1_id__exact=pk_user, u2_id__exact=pk_target) if f: f.delete() return Response({'resMsg': '移除成功'}) else: return Response({'resMsg': 'TA没有关注你'}) else: return Response({'resMsg': 'sType参数错误'}) else: return Response({'resMsg': '没有这个用户'}) else: return Response({'resMsg': '请求用户或密码错误'}) else: return Response({'resMsg': '获取失败'})
def test_activitypub_create_post(self, mock_get, mock_post): mock_get.side_effect = [self.create, self.actor] mock_post.return_value = requests_response('abc xyz') Follower.get_or_create('orig', 'https://mastodon/aaa') Follower.get_or_create('orig', 'https://mastodon/bbb', last_follow=json.dumps({ 'actor': { 'publicInbox': 'https://public/inbox', 'inbox': 'https://unused', } })) Follower.get_or_create('orig', 'https://mastodon/ccc', last_follow=json.dumps({ 'actor': { 'endpoints': { 'sharedInbox': 'https://shared/inbox', }, } })) Follower.get_or_create('orig', 'https://mastodon/ddd', last_follow=json.dumps( {'actor': { 'inbox': 'https://inbox', }})) self.datastore_stub.Flush() got = app.get_response('/webmention', method='POST', body=urllib.urlencode({ 'source': 'http://orig/post', 'target': 'https://fed.brid.gy/', })) self.assertEquals(200, got.status_int) mock_get.assert_has_calls((self.req('http://orig/post'), )) inboxes = ('https://public/inbox', 'https://shared/inbox', 'https://inbox') for call, inbox in zip(mock_post.call_args_list, inboxes): self.assertEquals((inbox, ), call[0]) self.assertEquals(self.create_as2, call[1]['json']) for inbox in inboxes: resp = Response.get_by_id('http://orig/post %s' % inbox) self.assertEqual('out', resp.direction, inbox) self.assertEqual('activitypub', resp.protocol, inbox) self.assertEqual('complete', resp.status, inbox) self.assertEqual(self.create_mf2, json.loads(resp.source_mf2), inbox)
def post(self, domain): logging.info('Got: %s', self.request.body) # parse and validate AS2 activity try: activity = json_loads(self.request.body) assert activity except (TypeError, ValueError, AssertionError): self.error("Couldn't parse body as JSON", exc_info=True) obj = activity.get('object') or {} if isinstance(obj, str): obj = {'id': obj} type = activity.get('type') if type == 'Accept': # eg in response to a Follow return # noop if type == 'Create': type = obj.get('type') elif type not in SUPPORTED_TYPES: self.error('Sorry, %s activities are not supported yet.' % type, status=501) # TODO: verify signature if there is one if type == 'Undo' and obj.get('type') == 'Follow': # skip actor fetch below; we don't need it to undo a follow return self.undo_follow(self.redirect_unwrap(activity)) elif type == 'Delete': id = obj.get('id') if isinstance(id, str): # assume this is an actor # https://github.com/snarfed/bridgy-fed/issues/63 for key in Follower.query().iter(keys_only=True): if key.id().split(' ')[-1] == id: key.delete() return # fetch actor if necessary so we have name, profile photo, etc for elem in obj, activity: actor = elem.get('actor') if actor and isinstance(actor, str): elem['actor'] = common.get_as2(actor).json() activity_unwrapped = self.redirect_unwrap(activity) if type == 'Follow': return self.accept_follow(activity, activity_unwrapped) # send webmentions to each target as1 = as2.to_as1(activity) self.send_webmentions(as1, proxy=True, protocol='activitypub', source_as2=json_dumps(activity_unwrapped))
def test_inbox_follow_accept(self, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='https://realize.be/') mock_get.side_effect = [ # source actor requests_response(FOLLOW_WITH_ACTOR['actor'], content_type=common.CONTENT_TYPE_AS2), # target post webmention discovery requests_response( '<html><head><link rel="webmention" href="/webmention"></html>' ), ] mock_post.return_value = requests_response() got = application.get_response( '/foo.com/inbox', method='POST', body=json_dumps(FOLLOW_WRAPPED).encode()) self.assertEqual(200, got.status_int) as2_headers = copy.deepcopy(common.HEADERS) as2_headers.update(common.CONNEG_HEADERS_AS2_HTML) mock_get.assert_has_calls((call(FOLLOW['actor'], headers=as2_headers, stream=True, timeout=15), )) # check AP Accept self.assertEqual(2, len(mock_post.call_args_list)) args, kwargs = mock_post.call_args_list[0] self.assertEqual(('http://follower/inbox', ), args) self.assertEqual(ACCEPT, kwargs['json']) # check webmention args, kwargs = mock_post.call_args_list[1] self.assertEqual(('https://realize.be/webmention', ), args) self.assertEqual( { 'source': 'http://localhost/render?source=https%3A%2F%2Fmastodon.social%2F6d1a&target=https%3A%2F%2Frealize.be%2F', 'target': 'https://realize.be/', }, kwargs['data']) resp = Response.get_by_id( 'https://mastodon.social/6d1a https://realize.be/') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(FOLLOW_WITH_ACTOR, json_loads(resp.source_as2)) # check that we stored a Follower object follower = Follower.get_by_id('realize.be %s' % (FOLLOW['actor'])) self.assertEqual('active', follower.status) self.assertEqual(FOLLOW_WRAPPED_WITH_ACTOR, json_loads(follower.last_follow))
def test_activitypub_create_with_image(self, mock_get, mock_post): create_html = self.create_html.replace( '</body>', '<img class="u-photo" src="http://im/age" />\n</body>') mock_get.side_effect = [ requests_response(create_html, content_type=CONTENT_TYPE_HTML), self.actor, ] mock_post.return_value = requests_response('abc xyz ') Follower.get_or_create('orig', 'https://mastodon/aaa', last_follow=json.dumps( {'actor': { 'inbox': 'https://inbox' }})) self.datastore_stub.Flush() got = app.get_response('/webmention', method='POST', body=urllib.urlencode({ 'source': 'http://orig/post', 'target': 'https://fed.brid.gy/', })) self.assertEquals(200, got.status_int) self.assertEquals(('https://inbox', ), mock_post.call_args[0]) create = copy.deepcopy(self.create_as2) create['object'].update({ 'image': [{ 'url': 'http://im/age', 'type': 'Image' }], 'attachment': [{ 'url': 'http://im/age', 'type': 'Image' }], }) self.assertEquals(create, mock_post.call_args[1]['json'])
def check_followers_updates(self, screen_name): print "Checking for new followers and unfollowers..." previous_followers = self.session.query(Follower).filter_by( is_following=True).all() previous_followers_ids = [ follower.twitter_id for follower in previous_followers ] current_followers = self.get_followers(screen_name) new_followers = [] new_unfollowers = [] for current_follower in current_followers: if current_follower not in previous_followers_ids: # New follower ! new_followers.append(current_follower) follower = self.api.get_user(current_follower) print "[%s] Found a new follower : %s [%s] (#%d)" \ % (datetime.today().strftime('%d/%m %H:%M'), follower.name, follower.screen_name, follower.id) self.session.add( Follower( name=follower.name, screen_name=follower.screen_name, twitter_id=follower.id, is_following=True, last_following=datetime.now(), )) # Discover the unfollowers for old_follower in previous_followers_ids: if old_follower not in current_followers: new_unfollowers.append(old_follower) # Unfollower! unfollower = self.session.query(Follower).filter_by( twitter_id=old_follower).one() follower = self.api.get_user(old_follower) print "[%s] Found an unfollower : %s [%s] (#%d)" % \ (datetime.today().strftime('%d/%m %H:%M'), unfollower.name, unfollower.screen_name, unfollower.id) unfollower.is_following = False # Close the session self.session.commit() self.session.close() if len(new_followers) == 0: print "no new followers" if len(new_unfollowers) == 0: print "no new unfollowers"
def get(self, whom_name): who = self.current_user whom = db.query(User).filter(User.name == whom_name).first() if whom is None or whom is who: raise tornado.web.HTTPError(404) follower = db.query(Follower).filter( sa.and_(Follower.who_id == who.id, Follower.whom_id == whom.id)).first() if follower is not None: raise tornado.web.HTTPError(404) db.add(Follower(who_id=who.id, whom_id=whom.id)) db.commit() self.redirect(self.next_url) return
def test_inbox_delete_actor(self, mock_head, mock_get, mock_post): follower = Follower.get_or_create('realize.be', DELETE['actor']) Follower.get_or_create('snarfed.org', DELETE['actor']) # other unrelated follower other = Follower.get_or_create('realize.be', 'https://mas.to/users/other') self.assertEqual(3, Follower.query().count()) got = application.get_response('/realize.be/inbox', method='POST', body=json_dumps(DELETE).encode()) self.assertEqual(200, got.status_int) self.assertEqual([other], Follower.query().fetch())
def update(self): previous_followers = self.session.query(Follower).filter_by( is_following=True).all() previous_followers_ids = [ follower.twitter_id for follower in previous_followers ] current_followers = self.api.GetFollowers() current_followers_ids = [ follower.GetId() for follower in current_followers ] #print "Found %d followers" % len(current_followers) # Add the new followers for follower in current_followers: if follower.GetId() not in previous_followers_ids: # New follower ! print "[%s] Found a new follower : %s [%s] (#%d)" \ % (datetime.today().strftime('%d/%m %H:%M'), follower.GetName(), follower.GetScreenName(), follower.GetId()) self.session.add( Follower( name=follower.GetName(), screen_name=follower.GetScreenName(), twitter_id=follower.GetId(), is_following=True, last_following=datetime.now(), )) # Discover the unfollowers for old_follower in previous_followers: if old_follower.twitter_id not in current_followers_ids: # Unfollower ! print "[%s] Found an unfollower : %s [%s] (#%d)" % \ (datetime.today().strftime('%d/%m %H:%M'), old_follower.name, old_follower.screen_name, old_follower.twitter_id) old_follower.is_following = False # Close the session self.session.commit() self.session.close()
def follow_user(user_id): whom_id = user_id try: whom = db.session.query(User).filter_by(id=whom_id).first().name if session['user_id'] != whom_id: new_follow = Follower( session['user_id'], whom_id ) try: db.session.add(new_follow) db.session.commit() flash('You are now following {}'.format(whom)) return redirect(url_for('tweets.tweet')) except IntegrityError: flash('You are already following {}'.format(whom)) return redirect(url_for('tweets.tweet')) else: flash('No use following yourself. You will still see your 1grams anyway. :)') return redirect(url_for('tweets.tweet')) except AttributeError: flash('That user does not exist.') return redirect(url_for('tweets.tweet'))
def fetch_followers(username: str, api: tweepy.API): """ Use tweepy to fetch user's followers' ids and then fetch their user objects and save to the db. params: username(str) - username of user to fetch followers for api(tweepy.API) - tweepy api instance """ total_followers = api.me().followers_count print("Fetching {} followers".format(total_followers)) db.create_all() follower_ids = [] print("Fetching follower ids!") for id in rate_limit_handler( tweepy.Cursor(api.followers_ids, count=5000).items()): follower_ids.append(id) print("Fetching user objects from ids!") for list_of_100 in list(divide_into_chunks(follower_ids, 100)): for i, follower in enumerate(api.lookup_users(user_ids=list_of_100)): follower_dict = dict( (k, follower.__dict__[k]) for k in follower_keys) user = User.query.filter_by(username=username).first() if not user: user = User(username=username) follower = Follower(**follower_dict) user.followers.append(follower) db.session.add(user) db.session.commit() print_progress_bar( i + 1, total_followers, prefix="Fetching {}/{} Followers".format( i + 1, total_followers), suffix="Fetched", ) print("Done!")
source = int(session.get("uid")) if source == target: return make_response(-1, message="禁止关注你自己") if (query := db.session.query(Follower).filter_by(source=int( session.get("uid")), target=target)).count(): query.delete() followed = False else: if db.session.query(Follower).filter_by( source=source).count() >= config.FOLLOWING_COUNT_LIMIT: return make_response( -1, message=f"你最多只能关注 {config.FOLLOWING_COUNT_LIMIT} 个人") db.session.add(Follower(source=session.get('uid'), target=target)) followed = True db.session.commit() return make_response(0, message="操作完成", data={"followed": followed}) @app.route("/api/user/get_follower_list", methods=["POST"]) @unpack_argument def api_user_get_follower_list(target: int, page: int = 1): """ { "code": "data":[ { "uid":"", "username":"",
def _activitypub_targets(self): """ Returns: list of (Response, string inbox URL) """ # if there's in-reply-to, like-of, or repost-of, they're the targets. # otherwise, it's all followers' inboxes. targets = self._targets() if not targets: # interpret this as a Create or Update, deliver it to followers inboxes = [] for follower in Follower.query().filter( Follower.key > Key('Follower', self.source_domain + ' '), Follower.key < Key( 'Follower', self.source_domain + chr(ord(' ') + 1))): if follower.status != 'inactive' and follower.last_follow: actor = json_loads(follower.last_follow).get('actor') if actor and isinstance(actor, dict): inboxes.append( actor.get('endpoints', {}).get('sharedInbox') or actor.get('publicInbox') or actor.get('inbox')) return [(Response.get_or_create(source=self.source_url, target=inbox, direction='out', protocol='activitypub', source_mf2=json_dumps( self.source_mf2)), inbox) for inbox in inboxes if inbox] resps_and_inbox_urls = [] for target in targets: # fetch target page as AS2 object try: self.target_resp = common.get_as2(target) except (requests.HTTPError, exc.HTTPBadGateway) as e: self.target_resp = getattr(e, 'response', None) if self.target_resp and self.target_resp.status_code // 100 == 2: content_type = common.content_type(self.target_resp) or '' if content_type.startswith('text/html'): # TODO: pass e.response to try_salmon()'s target_resp continue # give up raise target_url = self.target_resp.url or target resp = Response.get_or_create(source=self.source_url, target=target_url, direction='out', protocol='activitypub', source_mf2=json_dumps( self.source_mf2)) # find target's inbox target_obj = self.target_resp.json() resp.target_as2 = json_dumps(target_obj) inbox_url = target_obj.get('inbox') if not inbox_url: # TODO: test actor/attributedTo and not, with/without inbox actor = (util.get_first(target_obj, 'actor') or util.get_first(target_obj, 'attributedTo')) if isinstance(actor, dict): inbox_url = actor.get('inbox') actor = actor.get('url') or actor.get('id') if not inbox_url and not actor: self.error( 'Target object has no actor or attributedTo with URL or id.' ) elif not isinstance(actor, str): self.error( 'Target actor or attributedTo has unexpected url or id object: %r' % actor) if not inbox_url: # fetch actor as AS object actor = common.get_as2(actor).json() inbox_url = actor.get('inbox') if not inbox_url: # TODO: probably need a way to save errors like this so that we can # return them if ostatus fails too. # self.error('Target actor has no inbox') continue inbox_url = urllib.parse.urljoin(target_url, inbox_url) resps_and_inbox_urls.append((resp, inbox_url)) return resps_and_inbox_urls
def test_activitypub_create_post(self, mock_get, mock_post): mock_get.side_effect = [self.create, self.actor] mock_post.return_value = requests_response('abc xyz') Follower.get_or_create('orig', 'https://mastodon/aaa') Follower.get_or_create('orig', 'https://mastodon/bbb', last_follow=json_dumps({ 'actor': { 'publicInbox': 'https://public/inbox', 'inbox': 'https://unused', } })) Follower.get_or_create('orig', 'https://mastodon/ccc', last_follow=json_dumps({ 'actor': { 'endpoints': { 'sharedInbox': 'https://shared/inbox', }, } })) Follower.get_or_create('orig', 'https://mastodon/ddd', last_follow=json_dumps( {'actor': { 'inbox': 'https://inbox', }})) Follower.get_or_create('orig', 'https://mastodon/eee', status='inactive', last_follow=json_dumps( {'actor': { 'inbox': 'https://unused/2', }})) Follower.get_or_create( 'orig', 'https://mastodon/fff', last_follow=json_dumps({ 'actor': { # dupe of eee; should be de-duped 'inbox': 'https://inbox', } })) got = self.client.post('/webmention', data={ 'source': 'http://orig/post', 'target': 'https://fed.brid.gy/', }) self.assertEqual(200, got.status_code) mock_get.assert_has_calls((self.req('http://orig/post'), )) inboxes = ('https://inbox', 'https://public/inbox', 'https://shared/inbox') self.assertEqual(len(inboxes), len(mock_post.call_args_list)) for call, inbox in zip(mock_post.call_args_list, inboxes): self.assertEqual((inbox, ), call[0]) self.assertEqual(self.create_as2, json_loads(call[1]['data'])) for inbox in inboxes: resp = Response.get_by_id('http://orig/post %s' % inbox) self.assertEqual('out', resp.direction, inbox) self.assertEqual('activitypub', resp.protocol, inbox) self.assertEqual('complete', resp.status, inbox) self.assertEqual(self.create_mf2, json_loads(resp.source_mf2), inbox)