📜 ⬆️ ⬇️

Social network on Android for a few days off - part II (server)

Summary of the first part


In response to the incessant boom of mobile social apps, my friends and I decided to get together in a mini hackathon and write another social network on Android in order to outline the range of common issues and offer a skeleton from which everyone can make something new and original. In the first part, we looked at the client interface, network requests, the friend graph, and image processing.
In this article we will briefly tell you about uploading photos to the cloud storage, delivering push notifications and a queue of asynchronous tasks on the server.

Content


Introduction
check in
Contact Sync
Upload photos
Push notifications
Asynchronous Task Queues
Conclusion

Introduction


The server part of the application performs the functions of registering users, synchronizing the list of contacts and managing the list of friends, downloading and post-processing photos, managing and issuing comments / likes, sending push notifications. Consider these questions in more detail.

check in


When registering, the user is required to specify the name and phone number, as well as optionally choose an avatar. Since identification of users occurs on the contact book, then the important aspect is the verification of the specified phone, so we added SMS verification . You can choose your service for sending SMS from this article .

Contact Sync


To build a graph of friends on the server, the contact lists of users are recorded and compared with the phone numbers of users specified during registration. All contact lists are stored in a hashed form. Phone numbers must be given in normal form, which is used by the library libphonenumber from Google.

Code 1. An example of normalization in libphonenumber
String strRawPhone = "8-903-1234567"; PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); try { PhoneNumber swissNumberProto = phoneUtil.parse(swissNumberStr, "RU"); } catch (NumberParseException e) { System.err.println("NumberParseException was thrown: " + e.toString()); } System.out.println(phoneUtil.format(swissNumberProto, PhoneNumberFormat.E164)); //: +79031234567 


It is worth noting one nuance - the country code is defined in ISO-3166 format relative to the user's device, i.e. even if in my contact book there are phone numbers of other countries, then when these numbers are normalized, it is necessary to use the country code of the “registry” of my device's SIM card - RU.

Comparison of phones occurs in one of two cases:

For the described scenario, two tables are created on the server database — one for the contact list itself and one for the list of confirmed friends (the graph of friends itself). This scheme allows you to change existing contacts without breaking the previously formed edges of the graph of friends.
Code 2. Database schema - contacts and friends
db / schema.rb
  create_table "contacts", force: true do |t| t.string "public_id" t.string "contact_key" t.datetime "created_at" t.datetime "updated_at" end create_table "friends", force: true do |t| t.string "public_id_src" t.string "public_id_dest" t.integer "status" t.datetime "created_at" t.datetime "updated_at" t.string "contact_key" end 



Upload photos


We chose two options as a photo repository - a free account (free tier) AWS S3 as the main server and our own server as a backup (for example, in case the requests are exceeded in the free S3 account).
Figure 1. Uploading images on AWS S3
image

Before downloading, the client requests a temporary public link from the server with write permissions, downloads this link directly to S3, and then reports to the server about the successful download. We used aws-sdk gem to work with AWS S3. Before work, you must create an account in AWS Web Services (at the time of development it was possible to create a free test account for 5GB and 20,000 requests) and get a pair of ACCESS_KEY / SECRET_ACCESS_KEY keys
Code 3. Request a public link in aws-sdk
lib / s3.rb
 require 'aws-sdk' class S3Storage ... def self.get_presigned_url(key) s3 = Aws::S3::Resource.new( :access_key_id => APP_CONFIG['s3_access_key_id'], :secret_access_key => APP_CONFIG['s3_secret_access_key'], :region => APP_CONFIG['s3_region']) obj = s3.bucket(APP_CONFIG['s3_bucket']).object(APP_CONFIG['s3_prefix'] + "/" + key) obj.presigned_url(:put, acl: 'public-read', expires_in: 3600) end ... 


After the client reports that the photo has been successfully uploaded, our server downloads it asynchronously, makes two miniatures using rmagick gem and saves it back to the cloud storage. Thumbnails are used to facilitate traffic on a mobile device when viewing images in the tape.
Code 4. An example of creating thumbnails in rmagick
lib / uploader.rb
 require 'aws-sdk' require 'open-uri' require 's3' class Uploader @queue = :upload def self.perform(img_id) ... image = Image.where(image_id: img_id).first image_original = Magick::Image.from_blob(open(image.url_original).read).first image_medium = image_original.resize_to_fit(Image::MEDIUM_WIDTH, medium_height) image_medium.write( filepath_medium ){self.quality=100} ... end end 


After the uploaded photos are processed, a push notification is sent to all subscribers.

Push notifications


When uploading new photos or adding comments, real-time push notifications are sent to the user's subscribers. The most popular and fairly simple way to deliver push notifications to Android is GCM - Google Cloud Messaging . Before using the service, you need to register your project in the developer console , get the API key and the Project Number . The API key is used to authorize the application server when requesting GCM, it is added to the HTTP request header.
')
From the client’s side, the unique identifier of the recipient of notifications is PushID , which is obtained by contacting the Android device directly to the GCM server via the GoogleCloudMessaging SDK from the Android device, and you must specify the ProjectID received earlier. The received PushID is sent to our application server and is subsequently used in the delivery of notifications.
Figure 2. The sequence of registration of the new PushID
image

Code 5. Example of registering a new PushID (client)
class MainActivityHandler
  public void registerPushID() { AsyncTask task = new AsyncTask() { @Override protected Object doInBackground(Object[] params) { String strPushID = ""; try { if (gcm == null) { gcm = GoogleCloudMessaging.getInstance(activity); } strPushID = gcm.register(Constants.PUSH_SENDER_ID); Log.d(LOG_TAG, "Received push id = " + strPushID); } catch (IOException ex) { Log.d(LOG_TAG, "Error: " + ex.getMessage()); } return strPushID; } @Override protected void onPostExecute(Object res) { final String strPushID = res != null ? (String) res : ""; if (!strPushID.isEmpty()) { UserProfile profile = new UserProfile(); profile.pushid = strPushID; Log.d(LOG_TAG, "Sending pushId " + strPushID + " to server"); ServerInterface.updateProfileRequest(activity, profile, new Response.Listener<String>() { @Override public void onResponse(String response) { Photobook.getPreferences().strPushRegID = strPushID; Photobook.getPreferences().savePreferences(); Log.d(LOG_TAG, "Delivered pushId to server"); } }, null); } } }; task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } 


The connection between the application server and the GCM can be made in two ways - via XMPP and HTTP . The first option is asynchronous (allows you to send multiple messages without waiting for confirmation of the previous ones), and also supports upstream / downstream two-way communication. HTTP only supports synchronous downstream requests, but allows sending notifications to several recipients at once.
Figure 3. The sequence of delivery of push-notifications
image


Code 6. Example of sending push notifications (HTTP)
lib / push.rb
 require 'net/http' class PushSender def self.perform(id, event, msg) user = User.where(id: id).first http = Net::HTTP.new('android.googleapis.com', 80) request = Net::HTTP::Post.new('/gcm/send', {'Content-Type' => 'application/json', 'Authorization' => 'key=' + APP_CONFIG['google_api_key']}) data = {:registration_ids => [user.pushid], :data => {:event => event, :msg => msg}} request.body = data.to_json response = http.request(request) end end 



Asynchronous Task Queues


To speed up interaction with the client, some tasks on the server are performed in the background. In particular, it is sending push notifications, as well as image scaling. For such tasks, we chose resque gem . A list of queuing processing solutions and a brief description can be found here . We chose resque for its ease of installation and configuration, support for persistence using the redis database, the presence of a minimalist web interface. After starting the rails server, you must separately start the resque queue handler in the following way:
 QUEUE=* rake environment resque:work 

After that, the setting of new tasks in the queue is carried out in the following way (On the example of sending push notifications)
Code 7. Example of setting the task in the queue
app / controllers / image_controller.rb
 #Crop and save uploaded file def create img_id = request.headers['imageid'] ... Resque.enqueue(Uploader, img_id) ... end 

lib / uploader.rb
 require 'aws-sdk' require 'open-uri' require 's3' class Uploader @queue = :upload def self.perform(img_id) ... author = User.where(id: image.author_id).first if (author != nil) followers = Friend.where(public_id_dest: author.id.to_s, status: Friend::STATUS_FRIEND) followers.each do |follower| data = {:image_id => img_id, :author => JSON.parse(author.profile), :image => image} PushSender.perform(follower.public_id_src, PushSender::EVENT_NEW_IMAGE, data) end end end end 



Conclusion


Work on the application was carried out without the goal of gaining commercial benefits and solely for the sake of self-interest, as well as to strengthen teamwork skills. The format of our meetings was similar to the weekend hackathon; every day we tried to implement a specific module of the application. We will be happy if you have comments or suggestions for improving the project, and also plan to continue such hackathons, so if you are a novice backend / web / Android developer and you have an interest to participate in this format offline meetings in Moscow or remotely then email us via any communication channels.
This is us
image

PS It should be noted that writing a new social network is not a difficult task and, if there is a desire, even a novice Android developer is available. Instead of your own backend, you can use ready-made solutions from Google Apps Engine or Heroku . The development of the concept, operational support and scaling of the network due to the growing number of users is much more difficult. Perhaps we will consider these issues in future articles.

github
Android client
Ruby on rails server

Good luck and good week to all!

Source: https://habr.com/ru/post/258111/


All Articles