Skip to content

Commit

Permalink
Merge pull request #2177 from devos50/subscribe_channel_endpoint
Browse files Browse the repository at this point in the history
Implemented endpoint to subscribe to and unsubscribe from a channel
  • Loading branch information
whirm committed May 13, 2016
2 parents eec02c1 + ba11a56 commit 1c8304f
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 20 deletions.
126 changes: 108 additions & 18 deletions Tribler/Core/Modules/restapi/channels_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import json
import time

from twisted.web import resource
from twisted.web import http, resource

from Tribler.Core.simpledefs import NTFY_CHANNELCAST
from Tribler.community.allchannel.community import AllChannelCommunity


VOTE_UNSUBSCRIBE = 0
VOTE_SUBSCRIBE = 2


class BaseChannelsEndpoint(resource.Resource):
Expand All @@ -16,10 +22,37 @@ def __init__(self, session):
self.session = session
self.channel_db_handler = self.session.open_dbhandler(NTFY_CHANNELCAST)

@staticmethod
def return_404(request, message="the channel with the provided cid is not known"):
"""
Returns a 404 response code if your channel has not been created.
"""
request.setResponseCode(http.NOT_FOUND)
return json.dumps({"error": message})

def get_channel_from_db(self, cid):
"""
Returns information about the channel from the database. Returns None if the channel with given cid
does not exist.
"""
channels_list = self.channel_db_handler.getChannelsByCID([cid])
if not channels_list:
return None
return channels_list[0]

def vote_for_channel(self, cid, vote):
"""
Make a vote in the channel specified by the cid
"""
for community in self.session.get_dispersy_instance().get_communities():
if isinstance(community, AllChannelCommunity):
community.disp_create_votecast(cid, vote, int(time.time()))
break

def convert_db_channel_to_json(self, channel):
return {"id": channel[0], "dispersy_cid": channel[1].encode('hex'), "name": channel[2], "description": channel[3],
"votes": channel[5], "torrents": channel[4], "spam": channel[6], "modified": channel[8],
"subscribed": (channel[7] == 2)}
"subscribed": (channel[7] == VOTE_SUBSCRIBE)}


class ChannelsEndpoint(BaseChannelsEndpoint):
Expand All @@ -37,30 +70,87 @@ def __init__(self, session):

class ChannelsSubscribedEndpoint(BaseChannelsEndpoint):
"""
A GET request to this endpoint returns all the channels the user is subscribed to.
Example GET response:
{
"subscribed": [{
"id": 3,
"dispersy_cid": "da69aaad39ccf468aba2ab9177d5f8d8160135e6",
"name": "My fancy channel",
"description": "A description of this fancy channel",
"subscribed": True,
"votes": 23,
"torrents": 3,
"spam": 5,
"modified": 14598395,
}, ...]
}
This class is responsible for requests regarding the subscriptions to channels.
"""
def getChild(self, path, request):
return ChannelsModifySubscriptionEndpoint(self.session, path)

def render_GET(self, request):
"""
A GET request to this endpoint returns all the channels the user is subscribed to.
Example GET response:
{
"subscribed": [{
"id": 3,
"dispersy_cid": "da69aaad39ccf468aba2ab9177d5f8d8160135e6",
"name": "My fancy channel",
"description": "A description of this fancy channel",
"subscribed": True,
"votes": 23,
"torrents": 3,
"spam": 5,
"modified": 14598395,
}, ...]
}
"""
subscribed_channels_db = self.channel_db_handler.getMySubscribedChannels(include_dispersy=True)
results_json = [self.convert_db_channel_to_json(channel) for channel in subscribed_channels_db]
return json.dumps({"subscribed": results_json})


class ChannelsModifySubscriptionEndpoint(BaseChannelsEndpoint):
"""
This class is responsible for methods that modify the list of RSS feed URLs (adding/removing feeds).
"""

def __init__(self, session, cid):
BaseChannelsEndpoint.__init__(self, session)
self.cid = bytes(cid.decode('hex'))

def render_PUT(self, request):
"""
Subscribe to a specific channel. Returns error 409 if you are already subscribed to this channel.
Example response:
{
"subscribed" : True
}
"""
request.setHeader('Content-Type', 'text/json')
channel_info = self.get_channel_from_db(self.cid)
if channel_info is None:
return ChannelsModifySubscriptionEndpoint.return_404(request)

if channel_info[7] == VOTE_SUBSCRIBE:
request.setResponseCode(http.CONFLICT)
return json.dumps({"error": "you are already subscribed to this channel"})

self.vote_for_channel(self.cid, VOTE_SUBSCRIBE)
return json.dumps({"subscribed": True})

def render_DELETE(self, request):
"""
Unsubscribe from a specific channel. Returns error 404 if you are not subscribed to this channel.
Example response:
{
"unsubscribed" : True
}
"""
request.setHeader('Content-Type', 'text/json')
channel_info = self.get_channel_from_db(self.cid)
if channel_info is None:
return ChannelsModifySubscriptionEndpoint.return_404(request)

if channel_info[7] != VOTE_SUBSCRIBE:
return ChannelsModifySubscriptionEndpoint.return_404(request,
message="you are not subscribed to this channel")

self.vote_for_channel(self.cid, VOTE_UNSUBSCRIBE)
return json.dumps({"unsubscribed": True})


class ChannelsDiscoveredEndpoint(BaseChannelsEndpoint):
"""
A GET request to this endpoint returns all channels discovered in Tribler.
Expand Down
122 changes: 120 additions & 2 deletions Tribler/Test/Core/Modules/RestApi/test_channels_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import json
import time
from Tribler.Core.Modules.restapi.channels_endpoint import VOTE_SUBSCRIBE, VOTE_UNSUBSCRIBE

from Tribler.Core.Utilities.twisted_thread import deferred
from Tribler.Core.simpledefs import NTFY_CHANNELCAST, NTFY_VOTECAST
from Tribler.Test.Core.Modules.RestApi.base_api_test import AbstractApiTest
from Tribler.community.allchannel.community import AllChannelCommunity
from Tribler.dispersy.dispersy import Dispersy
from Tribler.dispersy.endpoint import ManualEnpoint
from Tribler.dispersy.member import DummyMember
from Tribler.dispersy.util import blocking_call_on_reactor_thread


class TestChannelsEndpoint(AbstractApiTest):
class AbstractTestChannelsEndpoint(AbstractApiTest):

def setUp(self, autoload_discovery=True):
super(TestChannelsEndpoint, self).setUp(autoload_discovery)
super(AbstractTestChannelsEndpoint, self).setUp(autoload_discovery)
self.channel_db_handler = self.session.open_dbhandler(NTFY_CHANNELCAST)
self.votecast_db_handler = self.session.open_dbhandler(NTFY_VOTECAST)
self.channel_db_handler._get_my_dispersy_cid = lambda: "myfakedispersyid"
Expand All @@ -20,6 +26,9 @@ def insert_channel_in_db(self, dispersy_cid, peer_id, name, description):
def vote_for_channel(self, cid, vote_time):
self.votecast_db_handler.on_votes_from_dispersy([[cid, None, 'random', 2, vote_time]])


class TestChannelsEndpoint(AbstractTestChannelsEndpoint):

@deferred(timeout=10)
def test_channels_unknown_endpoint(self):
"""
Expand Down Expand Up @@ -77,3 +86,112 @@ def test_get_discovered_channels(self):
self.insert_channel_in_db('rand%d' % i, 42 + i, 'Test channel %d' % i, 'Test description %d' % i)

return self.do_request('channels/discovered', expected_code=200).addCallback(self.verify_channels)


class TestChannelsSubscriptionEndpoint(AbstractTestChannelsEndpoint):

def setUp(self, autoload_discovery=True):
"""
The startup method of this class creates a fake Dispersy instance with a fake AllChannel community. It also
inserts some random channels so we have some data to work with.
"""
super(TestChannelsSubscriptionEndpoint, self).setUp(autoload_discovery)
self.expected_votecast_cid = None
self.expected_votecast_vote = None
self.create_votecast_called = False

self.session.get_dispersy = lambda: True
self.session.lm.dispersy = Dispersy(ManualEnpoint(0), self.getStateDir())
self.create_fake_allchannel_community()

for i in xrange(0, 10):
self.insert_channel_in_db('rand%d' % i, 42 + i, 'Test channel %d' % i, 'Test description %d' % i)

def on_dispersy_create_votecast(self, cid, vote, _):
"""
Check whether we have the expected parameters when this method is called.
"""
self.assertEqual(cid, self.expected_votecast_cid)
self.assertEqual(vote, self.expected_votecast_vote)
self.create_votecast_called = True

@blocking_call_on_reactor_thread
def create_fake_allchannel_community(self):
"""
This method creates a fake AllChannel community so we can check whether a request is made in the community
when doing stuff with a channel.
"""
self.session.lm.dispersy._database.open()
fake_member = DummyMember(self.session.lm.dispersy, 1, "a" * 20)
member = self.session.lm.dispersy.get_new_member(u"curve25519")
fake_community = AllChannelCommunity(self.session.lm.dispersy, fake_member, member)
fake_community.disp_create_votecast = self.on_dispersy_create_votecast
self.session.lm.dispersy._communities = {"allchannel": fake_community}

def tearDown(self):
self.session.lm.dispersy = None
super(TestChannelsSubscriptionEndpoint, self).tearDown()

@deferred(timeout=10)
def test_subscribe_channel_not_exist(self):
"""
Testing whether the API returns an error when subscribing if the channel with the specified CID does not exist
"""
return self.do_request('channels/subscribed/abcdef', expected_code=404, request_type='PUT')

@deferred(timeout=10)
def test_subscribe_channel_already_subscribed(self):
"""
Testing whether the API returns error 409 when subscribing to an already subscribed channel
"""
cid = self.insert_channel_in_db('rand1', 42, 'Test channel', 'Test description')
self.vote_for_channel(cid, int(time.time()))

return self.do_request('channels/subscribed/%s' % 'rand1'.encode('hex'), expected_code=409, request_type='PUT')

@deferred(timeout=10)
def test_subscribe_channel(self):
"""
Testing whether the API creates a request in the AllChannel community when subscribing to a channel
"""
def verify_votecast_made(_):
self.assertTrue(self.create_votecast_called)

expected_json = {"subscribed": True}
self.expected_votecast_cid = 'rand1'
self.expected_votecast_vote = VOTE_SUBSCRIBE
return self.do_request('channels/subscribed/%s' % 'rand1'.encode('hex'), expected_code=200,
expected_json=expected_json, request_type='PUT').addCallback(verify_votecast_made)

@deferred(timeout=10)
def test_unsubscribe_channel_not_exist(self):
"""
Testing whether the API returns an error when unsubscribing if the channel with the specified CID does not exist
"""
return self.do_request('channels/subscribed/abcdef', expected_code=404, request_type='DELETE')

@deferred(timeout=10)
def test_unsubscribe_channel_not_subscribed(self):
"""
Testing whether the API returns error 404 when unsubscribing from an already unsubscribed channel
"""
self.insert_channel_in_db('rand1', 42, 'Test channel', 'Test description')
return self.do_request('channels/subscribed/%s' % 'rand1'.encode('hex'),
expected_code=404, request_type='DELETE')

@deferred(timeout=10)
def test_unsubscribe_channel(self):
"""
Testing whether the API creates a request in the AllChannel community when unsubscribing from a channel
"""
def verify_votecast_made(_):
self.assertTrue(self.create_votecast_called)

cid = self.insert_channel_in_db('rand1', 42, 'Test channel', 'Test description')
self.vote_for_channel(cid, int(time.time()))

expected_json = {"unsubscribed": True}
self.expected_votecast_cid = 'rand1'
self.expected_votecast_vote = VOTE_UNSUBSCRIBE
return self.do_request('channels/subscribed/%s' % 'rand1'.encode('hex'), expected_code=200,
expected_json=expected_json, request_type='DELETE').addCallback(verify_votecast_made)
26 changes: 26 additions & 0 deletions doc/Tribler REST API.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ curl -X PUT -d "rss_feed_url=http://fakerssprovider.com/feed.rss" http://localho
| ---- | --------------- |
| GET /channels/discovered | Get all discovered channels in Tribler |
| GET /channels/subscribed | Get the channels you are subscribed to |
| PUT /channels/subscribed/{channelid} | Subscribe to a channel |
| DELETE /channels/subscribed/{channelid} | Unsubscribe from a channel |

### My Channel

Expand Down Expand Up @@ -86,6 +88,30 @@ Returns all the channels you are subscribed to.
}
```

## `PUT /channels/subscribed/{channelcid}`

Subscribe to a specific channel. Returns error 409 if you are already subscribed to this channel.

### Example response

```json
{
"subscribed" : True
}
```

## `DELETE /channels/subscribed/{channelcid}`

Unsubscribe from a specific channel. Returns error 404 if you are not subscribed to this channel.

### Example response

```json
{
"unsubscribed" : True
}
```

## `GET /mychannel/overview`

Returns an overview of the channel of the user. This includes the name, description and identifier of the channel.
Expand Down

0 comments on commit 1c8304f

Please sign in to comment.