From bfac635520f93a40233b11011ea691b7117cc947 Mon Sep 17 00:00:00 2001 From: Jose Toro Date: Thu, 11 Feb 2016 14:45:35 +0100 Subject: [PATCH] First release - Photo albums download - Private messages download --- .gitignore | 3 + LICENSE | 21 +++ logo.png | Bin 0 -> 13813 bytes pom.xml | 132 +++++++++++++++ src/main/scala/io/callate/gui/About.scala | 129 +++++++++++++++ src/main/scala/io/callate/gui/Login.scala | 82 ++++++++++ src/main/scala/io/callate/gui/MenuPanel.scala | 119 ++++++++++++++ src/main/scala/io/callate/gui/OSX.scala | 15 ++ .../scala/io/callate/gui/PhotoModal.scala | 42 +++++ src/main/scala/io/callate/main/Main.scala | 90 +++++++++++ .../scala/io/callate/model/TuDownloader.scala | 152 ++++++++++++++++++ src/main/scala/io/callate/model/Utils.scala | 22 +++ 12 files changed, 807 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 logo.png create mode 100644 pom.xml create mode 100644 src/main/scala/io/callate/gui/About.scala create mode 100644 src/main/scala/io/callate/gui/Login.scala create mode 100644 src/main/scala/io/callate/gui/MenuPanel.scala create mode 100644 src/main/scala/io/callate/gui/OSX.scala create mode 100644 src/main/scala/io/callate/gui/PhotoModal.scala create mode 100644 src/main/scala/io/callate/main/Main.scala create mode 100644 src/main/scala/io/callate/model/TuDownloader.scala create mode 100644 src/main/scala/io/callate/model/Utils.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..612c5bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +.idea +*.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..884d022 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 callate.io + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6b63497041cb938966a0c469586737a9c81a76f1 GIT binary patch literal 13813 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 UxM2tQDhu|Pxz&~oQ%BPO1Gh}&-v9sr literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2af930f --- /dev/null +++ b/pom.xml @@ -0,0 +1,132 @@ + + 4.0.0 + io.callate + tuDownloader + 1.0 + 2016 + + 2.11.7 + + + + + scala-tools.org + Scala-Tools Maven2 Repository + http://scala-tools.org/repo-releases + + + + + + scala-tools.org + Scala-Tools Maven2 Repository + http://scala-tools.org/repo-releases + + + + + org.scala-lang + scala-library + ${scala.version} + + + org.jsoup + jsoup + 1.8.3 + + + org.json4s + json4s-native_2.11 + 3.3.0 + + + org.scala-lang + scala-swing + 2.11.0-M7 + + + + + src/main/scala + + + org.scala-tools + maven-scala-plugin + + + + compile + testCompile + + + + + ${scala.version} + + -target:jvm-1.5 + + + + + org.apache.maven.plugins + maven-eclipse-plugin + + true + + ch.epfl.lamp.sdt.core.scalabuilder + + + ch.epfl.lamp.sdt.core.scalanature + + + org.eclipse.jdt.launching.JRE_CONTAINER + ch.epfl.lamp.sdt.launching.SCALA_CONTAINER + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2-beta-5 + + + jar-with-dependencies + + + + io.callate.main.Main + + + + + + package + + single + + + + + + + + . + + **/*.png + + + + + + + + org.scala-tools + maven-scala-plugin + + ${scala.version} + + + + + + diff --git a/src/main/scala/io/callate/gui/About.scala b/src/main/scala/io/callate/gui/About.scala new file mode 100644 index 0000000..09a2600 --- /dev/null +++ b/src/main/scala/io/callate/gui/About.scala @@ -0,0 +1,129 @@ +package io.callate.gui + +import java.awt.Color +import java.awt.Dimension +import java.awt.Image +import java.awt._ +import java.awt.event.{MouseEvent, MouseAdapter} +import java.io.IOException +import java.net.{URISyntaxException, URI} +import javax.swing.ImageIcon + +import scala.swing.Dialog +import scala.swing.Label +import scala.swing._ +import scala.swing.BorderPanel.Position._ + +object About extends Dialog { + title = "tuDownloader" + resizable = false + modal = true + + val imagePanel = new BorderPanel{ + layout(new Label { + icon = new ImageIcon(new ImageIcon(getClass.getClassLoader.getResource("logo.png")).getImage.getScaledInstance(200, 200, Image.SCALE_SMOOTH)) + preferredSize = new Dimension(100, 100) + }) = Center + } + + val createdBy = new Label { text = "tuDownloader v1.0 - "} + val callateName = new Label { + text = "callate.io" + foreground = Color.blue + } + callateName.peer.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) + callateName.peer.addMouseListener(new MouseAdapter() { + override def mouseClicked(e: MouseEvent) { + if (e.getClickCount > 0) { + if (Desktop.isDesktopSupported) { + val desktop: Desktop = Desktop.getDesktop + try { + val uri: URI = new URI("http://callate.io") + desktop.browse(uri) + } catch { + case _: IOException => + case _: URISyntaxException => + } + } + } + } + }) + + + val nurLabel = new Label { + text = "@subnurmality" + foreground = Color.blue + } + nurLabel.peer.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) + nurLabel.peer.addMouseListener(new MouseAdapter() { + override def mouseClicked(e: MouseEvent) { + if (e.getClickCount > 0) { + if (Desktop.isDesktopSupported) { + val desktop: Desktop = Desktop.getDesktop + try { + val uri: URI = new URI("http://twitter.com/subnurmality") + desktop.browse(uri) + } catch { + case _: IOException => + case _: URISyntaxException => + } + } + } + } + }) + + val toroLabel = new Label { + text = "@wynkth" + foreground = Color.blue + } + toroLabel.peer.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) + toroLabel.peer.addMouseListener(new MouseAdapter() { + override def mouseClicked(e: MouseEvent) { + if (e.getClickCount > 0) { + if (Desktop.isDesktopSupported) { + val desktop: Desktop = Desktop.getDesktop + try { + val uri: URI = new URI("http://twitter.com/wynkth") + desktop.browse(uri) + } catch { + case _: IOException => + case _: URISyntaxException => + } + } + } + } + }) + + val nurFlowPanel = new FlowPanel { + contents += new Label("UI") + contents += nurLabel + } + + val toroFlowPanel = new FlowPanel { + contents += new Label("Code") + contents += toroLabel + } + + val flowPanel = new FlowPanel { + contents += createdBy + contents += callateName + } + + val borderLayout = new BorderPanel { + layout(flowPanel) = North + layout(toroFlowPanel) = Center + layout(nurFlowPanel) = South + } + + val gridPanel = new BorderPanel { + layout(imagePanel) = Center + layout(borderLayout) = South + } + // Add the grid, set the size + contents = new BorderPanel { + layout(gridPanel) = Center + } + size = new Dimension(320, 260) + + peer.setLocationRelativeTo(null) +} \ No newline at end of file diff --git a/src/main/scala/io/callate/gui/Login.scala b/src/main/scala/io/callate/gui/Login.scala new file mode 100644 index 0000000..440be4e --- /dev/null +++ b/src/main/scala/io/callate/gui/Login.scala @@ -0,0 +1,82 @@ +package io.callate.gui + +import io.callate.main.Main +import scala.swing._ +import scala.swing.BorderPanel.Position._ +import event._ + +object Login extends MainFrame { + title = "tuDownloader" + resizable = false + + if (System.getProperty("os.name") != "Mac OS X") { + menuBar = new MenuBar { + contents += new MenuItem(Action("Acerca de..") { + About.open() + }) + } + } + + // Define components + val emailLabel = new Label { + text = "Email: " + } + + val passLabel = new Label { + text = "Contraseña: " + } + + val button = new Button { + text = "Conectar" + enabled = true + tooltip = "Haz click para iniciar sesión" + } + + val emailText = new TextField { + columns = 20 + text = "" + } + + val passText = new PasswordField { + columns = 20 + text = "" + } + + val gridPanel = new GridPanel(5, 1) { + contents += emailLabel + contents += emailText + contents += passLabel + contents += passText + contents += button + } + + // Define alignments + emailLabel.horizontalAlignment = Alignment.Left + passLabel.horizontalAlignment = Alignment.Left + + // Add the grid, set the size + contents = new BorderPanel { + layout(gridPanel) = Center + } + + // Listen to events + listenTo(button) + listenTo(emailText.keys) + listenTo(passText.keys) + + // Add the reactions to the events + reactions += { + case ButtonClicked(component) if component == button => Main.login(emailText.text, new String(passText.password)) + case KeyPressed(_, Key.Enter, _, _) => Main.login(emailText.text, new String(passText.password)) + } + + // Set frame location + peer.setLocationRelativeTo(null) + + def error() = { + Dialog.showMessage(contents.head, + "Error al conectar, asegúrate de que has introducido los datos correctamente", + title="Error", + Dialog.Message.Error) + } +} \ No newline at end of file diff --git a/src/main/scala/io/callate/gui/MenuPanel.scala b/src/main/scala/io/callate/gui/MenuPanel.scala new file mode 100644 index 0000000..940b82f --- /dev/null +++ b/src/main/scala/io/callate/gui/MenuPanel.scala @@ -0,0 +1,119 @@ +package io.callate.gui + +import javax.swing.table.AbstractTableModel + +import io.callate.main.Main + +import scala.swing._ +import scala.swing.BorderPanel.Position._ +import event._ + +class AlbumModel(var cells: Array[Array[Any]], val columns: Array[String]) extends AbstractTableModel { + def getRowCount: Int = cells.length + def getColumnCount: Int = columns.length + def getValueAt(row: Int, col: Int): AnyRef = cells(row)(col).asInstanceOf[AnyRef] + override def getColumnClass(column: Int) = getValueAt(0, column).getClass + override def isCellEditable(row: Int, column: Int) = if (column == 2) true else false + override def setValueAt(value: Any, row: Int, col: Int) { + cells(row)(col) = value + fireTableCellUpdated(row, col) + } + override def getColumnName(column: Int): String = columns(column).toString +} + +class MenuPanel(val albums: List[(String, String, Int)]) extends MainFrame { + if (System.getProperty("os.name") != "Mac OS X") { + menuBar = new MenuBar { + contents += new MenuItem(Action("Acerca de..") { + About.open() + }) + } + } + + title = "tuDownloader" + resizable = false + // Define components + val header = Array("Nombre del álbum", "Fotos", "Descargar") + val items = albums.map(_.productIterator.toList.slice(1, 3).++(List(true)).toArray).toArray + val albumsTable = new Table(items, header) { + model = new AlbumModel(items, header) + + // Set widths + peer.getColumnModel.getColumn(0).setPreferredWidth(400) + peer.getColumnModel.getColumn(1).setPreferredWidth(100) + peer.getColumnModel.getColumn(2).setPreferredWidth(100) + } + val albumsScroll = new ScrollPane(albumsTable) + val downloadAlbums = new Button { + text = "Descargar marcados" + enabled = true + tooltip = "Haz click para bajar los álbumes seleccionados" + } + val downloadPMs = new Button { + text = "Descargar MPs" + enabled = true + tooltip = "Haz click para bajar tus mensajes privados" + } + val deselectAll = new Button { + text = "Desmarcar todos" + enabled = true + tooltip = "Haz click para desmarcar todos tus álbumes" + } + val selectAll = new Button { + text = "Marcar todos" + enabled = true + tooltip = "Haz click para marcar todos tus álbumes" + } + val buttonTopPanel = new FlowPanel { + contents += deselectAll + contents += selectAll + } + val buttonBottomPanel = new FlowPanel { + contents += downloadAlbums + contents += downloadPMs + } + val buttonPanel = new BorderPanel { + layout(buttonTopPanel) = North + layout(buttonBottomPanel) = South + } + val gridPanel = new BorderPanel { + layout(albumsScroll) = Center + layout(buttonPanel) = South + } + // Add the grid, set the size + contents = new BorderPanel { + layout(gridPanel) = Center + } + size = new Dimension(600, 400) + // Listen to events + listenTo(downloadAlbums) + listenTo(downloadPMs) + listenTo(deselectAll) + listenTo(selectAll) + + // Add the reactions to the events + reactions += { + case ButtonClicked(component) if component == deselectAll => albums.indices.foreach(albumsTable.model.setValueAt(false, _, 2)) + case ButtonClicked(component) if component == selectAll => albums.indices.foreach(albumsTable.model.setValueAt(true, _, 2)) + case ButtonClicked(component) if component == downloadAlbums => downloadMarkedAlbumsIDs() + case ButtonClicked(component) if component == downloadPMs => + Main.downloadPrivateMessages() + Dialog.showMessage(contents.head, + "Mensajes privados descargados correctamente (Mensajes privados.zip)", + title="Mensajes privados", + Dialog.Message.Info) + + } + + peer.setLocationRelativeTo(null) + + def downloadMarkedAlbumsIDs() = { + val albumsTuple = albums.zipWithIndex.filter(p => albumsTable.model.getValueAt(p._2, 2).asInstanceOf[Boolean]).map(p => p._1) + val numPhotos = albumsTuple.map(_._3).sum + val albumsIds = albumsTuple.map(_._1) + + if (numPhotos != 0) { + Main.downloadAlbums(albumsIds, numPhotos) + } + } +} \ No newline at end of file diff --git a/src/main/scala/io/callate/gui/OSX.scala b/src/main/scala/io/callate/gui/OSX.scala new file mode 100644 index 0000000..5d7dff4 --- /dev/null +++ b/src/main/scala/io/callate/gui/OSX.scala @@ -0,0 +1,15 @@ +package io.callate.gui + +import com.apple.eawt.{Application, AboutHandler} +import com.apple.eawt.AppEvent.AboutEvent + + +class OSX { + System.setProperty("apple.awt.application.name", "tuDownloader") + val app = Application.getApplication + app.setAboutHandler(new AboutHandler { + override def handleAbout(aboutEvent: AboutEvent): Unit = { + About.open() + } + }) +} diff --git a/src/main/scala/io/callate/gui/PhotoModal.scala b/src/main/scala/io/callate/gui/PhotoModal.scala new file mode 100644 index 0000000..ebde7e9 --- /dev/null +++ b/src/main/scala/io/callate/gui/PhotoModal.scala @@ -0,0 +1,42 @@ +package io.callate.gui + +import scala.swing._ +import scala.swing.BorderPanel.Position._ + +import io.callate.main.Main + +class PhotoModal(total: Int) extends MainFrame { + title = "Descargando fotos..." + resizable = false + val progressBar = new ProgressBar() + val progressPane = new ScrollPane(progressBar) + val progressText = new Label() + + progressBar.preferredSize = new Dimension(400, 20) + progressText.peer.setText("0 / " + total) + + contents = new BorderPanel { + layout(progressPane) = Center + layout(progressText) = South + } + + progressBar.peer.setMaximum(total) + progressBar.peer.setMinimum(0) + + peer.pack() + peer.setLocationRelativeTo(null) + + + def newPhoto() = { + progressBar.peer.setValue(progressBar.peer.getValue + 1) + progressText.peer.setText(progressBar.peer.getValue + " / " + total) + + if (progressBar.peer.getValue == progressBar.peer.getMaximum) { + Dialog.showMessage(contents.head, + "Fotos descargadas correctamente (carpeta 'Fotos')", + title="Fotos", + Dialog.Message.Info) + Main.finishedPhotos() + } + } +} diff --git a/src/main/scala/io/callate/main/Main.scala b/src/main/scala/io/callate/main/Main.scala new file mode 100644 index 0000000..6f59e3c --- /dev/null +++ b/src/main/scala/io/callate/main/Main.scala @@ -0,0 +1,90 @@ +package io.callate.main + +import java.io.{FileNotFoundException, File} +import java.net.{URL, HttpURLConnection} +import java.nio.file.Paths +import javax.swing.JMenuBar + +import io.callate.model.{Utils, TuDownloader} +import io.callate.gui._ + +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits._ + +import scala.swing.{Swing, SimpleSwingApplication} + +object Main extends App { + if (System.getProperty("os.name").equals("Mac OS X")) { + new OSX() + } + val loginUI = Login + var menuUI: MenuPanel = null + var photoModal: PhotoModal = null + val tuDownloader = new TuDownloader() + var albumsTuple: List[(String, String, Int)] = null + + loginUI.visible = true + + def login(email: String, password: String) = { + try { + if (tuDownloader.login(email, password)) { + loginUI.visible = false + + albumsTuple = tuDownloader.getAlbums.map(x => (x._1, x._2._1, x._2._2)).toList + + menuUI = new MenuPanel(albumsTuple) + menuUI.visible = true + + } else { + loginUI.error() + } + } catch { + case e: Exception => loginUI.error() + } + + } + + def downloadAlbums(albumIds: List[String], total: Int) = { + val folder = "Fotos" + if (!new File(folder).exists) { + Utils.mkdir(folder) + } + + photoModal = new PhotoModal(total) + + menuUI.visible = false + photoModal.visible = true + + Future { + for (id <- albumIds) { + val albumName = tuDownloader.getAlbums.get(id).get._1 + if (!new File(folder, albumName).exists()) { + Utils.mkdirs(List(folder, albumName)) + } + var photoCount = 1 + for (photoURL <- tuDownloader.getPhotosAlbumURLs(id)) { + val photoImg = tuDownloader.getPhotoUrl(photoURL) + val httpURLConnection: HttpURLConnection = new URL(photoImg).openConnection().asInstanceOf[HttpURLConnection] + httpURLConnection.setRequestMethod("GET") + httpURLConnection.connect() + // Photos that result in 404 requests + if (httpURLConnection.getResponseCode != 404) { + val outputFile = Paths.get(folder, albumName, photoCount + ".jpg") + Utils.fileDownloader(httpURLConnection.getInputStream, outputFile) + photoCount += 1 + } + photoModal.newPhoto() + } + } + } + } + + def finishedPhotos() = { + photoModal.visible = false + menuUI.visible = true + } + + def downloadPrivateMessages() = { + Utils.fileDownloader(tuDownloader.getPrivateMessagesURL, "Mensajes privados.zip") + } +} diff --git a/src/main/scala/io/callate/model/TuDownloader.scala b/src/main/scala/io/callate/model/TuDownloader.scala new file mode 100644 index 0000000..612d69d --- /dev/null +++ b/src/main/scala/io/callate/model/TuDownloader.scala @@ -0,0 +1,152 @@ +package io.callate.model + +import java.util + +import org.json4s.native.JsonMethods._ +import org.jsoup.Connection.{Method, Response} +import org.jsoup.Jsoup + +import scala.collection.mutable + + +class TuDownloader { + private var csrf: String = "" + private var cookies: util.Map[String, String] = new util.HashMap[String, String]() + private val albums: mutable.Map[String, (String, Int)] = mutable.HashMap[String, (String, Int)]() + + def login(email: String, pass: String): Boolean = { + var response: Response = Jsoup.connect("https://www.tuenti.com/?m=Login") + .execute() + + cookies = response.cookies() + val csfr: String = response.parse().select("input[name=csfr]").attr("value") + + response = Jsoup.connect("https://www.tuenti.com/?m=Login&func=do_login") + .method(Method.POST) + .cookies(cookies) + .data("email", email) + .data("input_password", pass) + .data("csfr", csfr) + .execute() + + if (!response.cookies().containsKey("sid")) { + return false + } + + // Get cookies response (header) + cookies = response.cookies() + + // Add cookies generated by javascript response + cookies.put("redirect_url", "m=Profile&func=index") + cookies.put("tempHash", "m=Profile&func=index") + + response = Jsoup.connect("https://www.tuenti.com/") + .ignoreContentType(true) + .cookies(cookies) + .execute() + + val profilePage = response.parse().html() + val csfrIndex = profilePage.indexOf("csfr") + csrf = profilePage.substring(csfrIndex + 9, csfrIndex + 17) + val json_payload = parse(response.parse().select("#response_json_payload").text()) + + cookies.remove("temp_hash") + cookies.remove("redirect_url") + cookies.remove("ourl") + + loadAlbums(response) + + true + } + + private def loadAlbums(indexResponse: Response) = { + val pAlbums = indexResponse.parse().select("#albumSelector").select(".sel-block") + for(i <- 0 until pAlbums.size()) { + val href = pAlbums.get(i).attr("href") + val indexStart = href.lastIndexOf("y=") + 2 + val indexFinal = href.lastIndexOf("&") + val albumId = href.substring(indexStart, indexFinal) + val text = pAlbums.get(i).text() + val number = Integer.valueOf(text.substring(text.lastIndexOf("(") + 1, text.lastIndexOf(")")).replace(".","")) + val title = text.substring(0, text.lastIndexOf("(") - 1) + albums.put(albumId, (title, number)) + } + } + + def getAlbums = albums + + def getPhotosAlbumURLs(albumId: String): mutable.Set[String] = { + var response = Jsoup.connect("https://www-1.tuenti.com/index.cupcake.php?m=Albums&func=getAlbumPhotos&collection_key=" + albumId + "&ajax=1") + .ignoreContentType(true) + .cookies(cookies) + .execute() + val htmlAlbum = Jsoup.parse(compact(render(parse(response.body().substring(8)) \\ "renderOutput" \\ "albumPhotosContainer" \\ "html")).replace("\\", "")) + val photosElements = htmlAlbum.select("#albumBody").select("li") + val photos = mutable.HashSet[String]() + + for (i <- 0 until photosElements.size()) { + val id = photosElements.get(i).attr("id").substring(5) + photos.add(id) + } + + var i = 1 + var viewMore = "" + while (viewMore != "null") { + response = Jsoup.connect("https://www-1.tuenti.com/index.cupcake.php?m=Albums&func=getMorePhotosPage&collection_key=" + albumId + "&photos_page=" + i + "&ajax=1") + .ignoreContentType(true) + .cookies(cookies) + .execute() + + val moreJson = parse(response.body().substring(8)) + viewMore = compact(render(moreJson \\ "renderOutput" \\ "albumsViewMore" \\ "html")).replace("\\", "") + val htmlMore = compact(render(moreJson \\ "renderOutput" \\ "albumBody" \\ "html")).replace("\\", "") + + if (htmlMore != "null") { + val morePhotos = Jsoup.parse(htmlMore).select("li") + for (i <- 0 until morePhotos.size()) { + val id = morePhotos.get(i).attr("id").substring(5) + photos.add(id) + } + + i += 1 + } + } + + photos + } + + def getPhotoUrl(photoId: String): String = { + val response = Jsoup.connect("https://www-1.tuenti.com/index.cupcake.php?m=Photo&func=preloadPhotos&ajax=1") + .ignoreContentType(true) + .data("itemKey", photoId) + .data("backgrounded", "false") + .data("prefetchDirection", "10") + .data("offset", "0") + .data("pc", "{\"wt\":3}") + .data("csfr", csrf) + .data("csrf", csrf) + .method(Method.POST) + .cookies(cookies) + .execute() + + val photoJson = parse(response.body().substring(8)) + val photoUrl = compact(render(photoJson \\ "jsonData" \\ photoId \\ "url")) + val photoIndex = photoUrl.indexOf("\"", 8) + photoUrl.substring(8, photoIndex) + } + + def getPrivateMessagesURL: String = { + val response = Jsoup.connect("https://www-1.tuenti.com/?m=Messages&func=index&ajax=1") + .ignoreContentType(true) + .cookies(cookies) + .execute() + + val json = parse(response.body().substring(8)) + val html = Jsoup.parse(compact(render(json \\ "renderOutput" \\ "canvas" \\ "html")).replace("\\","")) + val button = html.select("button").attr("onclick") + val parent = button.indexOf("'") + val comma = button.lastIndexOf(",") + + button.substring(parent + 1, comma - 1) + } +} diff --git a/src/main/scala/io/callate/model/Utils.scala b/src/main/scala/io/callate/model/Utils.scala new file mode 100644 index 0000000..19a02a6 --- /dev/null +++ b/src/main/scala/io/callate/model/Utils.scala @@ -0,0 +1,22 @@ +package io.callate.model + +import java.io.{InputStream, FileOutputStream, BufferedOutputStream, File} +import java.net.URL +import java.nio.file.{Path, Files} + +import scala.sys.process._ + +object Utils { + def fileDownloader(url: String, filename: String) = { + new URL(url) #> new File(filename) !! + } + + def fileDownloader(is: InputStream, filename: Path) = { + Files.copy(is, filename) + } + + def mkdir(path: String) = new java.io.File(path).mkdirs + + def mkdirs(path: List[String]) = + path.tail.foldLeft(new File(path.head)){(a,b) => a.mkdir; new File(a,b)}.mkdir +}