From bacfbb4e64cddaf3da86563db76ab4618c18472e Mon Sep 17 00:00:00 2001 From: ccd0 Date: Fri, 2 Jan 2015 23:30:19 -0800 Subject: [PATCH] Move post fetching code into a class. --- src/General/Get.coffee | 181 ---------------------------- src/General/lib/classes.coffee | 1 + src/General/lib/fetcher.class | 182 +++++++++++++++++++++++++++++ src/Quotelinks/QuoteInline.coffee | 2 +- src/Quotelinks/QuotePreview.coffee | 2 +- 5 files changed, 185 insertions(+), 183 deletions(-) create mode 100644 src/General/lib/fetcher.class diff --git a/src/General/Get.coffee b/src/General/Get.coffee index e8fc138a9..574e7b8f4 100755 --- a/src/General/Get.coffee +++ b/src/General/Get.coffee @@ -70,184 +70,3 @@ Get = for script in $$ 'script:not([src])', d.head return script.textContent if /\bcooldowns *=/.test script.textContent '' - postClone: (boardID, threadID, postID, root, context) -> - if post = g.posts["#{boardID}.#{postID}"] - Get.insert post, root, context - return - - root.textContent = "Loading post No.#{postID}..." - if threadID - $.cache "//a.4cdn.org/#{boardID}/thread/#{threadID}.json", -> - Get.fetchedPost @, boardID, threadID, postID, root, context - else - Get.archivedPost boardID, postID, root, context - insert: (post, root, context) -> - # Stop here if the container has been removed while loading. - return unless root.parentNode - clone = post.addClone context, ($.hasClass root, 'dialog') - Main.callbackNodes Clone, [clone] - - # Get rid of the side arrows. - {nodes} = clone - $.rmAll nodes.root - $.add nodes.root, nodes.post - - $.rmAll root - $.add root, nodes.root - $.event 'PostsInserted' - fetchedPost: (req, boardID, threadID, postID, root, context) -> - # In case of multiple callbacks for the same request, - # don't parse the same original post more than once. - if post = g.posts["#{boardID}.#{postID}"] - Get.insert post, root, context - return - - {status} = req - unless status in [200, 304] - # The thread can die by the time we check a quote. - unless Get.archivedPost boardID, postID, root, context - $.addClass root, 'warning' - root.textContent = - if status is 404 - "Thread No.#{threadID} 404'd." - else - "Error #{req.statusText} (#{req.status})." - return - - {posts} = req.response - Build.spoilerRange[boardID] = posts[0].custom_spoiler - for post in posts - break if post.no is postID # we found it! - - if post.no isnt postID - # Cached requests can be stale and must be rechecked. - if req.cached - api = "//a.4cdn.org/#{boardID}/thread/#{threadID}.json" - $.cleanCache (url) -> url is api - $.cache api, -> - Get.fetchedPost @, boardID, threadID, postID, root, context - return - # The post can be deleted by the time we check a quote. - unless Get.archivedPost boardID, postID, root, context - $.addClass root, 'warning' - root.textContent = "Post No.#{postID} was not found." - return - - board = g.boards[boardID] or - new Board boardID - thread = g.threads["#{boardID}.#{threadID}"] or - new Thread threadID, board - post = new Post Build.postFromObject(post, boardID), thread, board - post.isFetchedQuote = true - Main.callbackNodes Post, [post] - Get.insert post, root, context - archivedPost: (boardID, postID, root, context) -> - return false unless Conf['Resurrect Quotes'] - return false unless url = Redirect.to 'post', {boardID, postID} - if /^https:\/\//.test(url) or location.protocol is 'http:' - $.cache url, - -> Get.parseArchivedPost @response, boardID, postID, root, context - , - responseType: 'json' - withCredentials: url.archive.withCredentials - return true - else if Conf['Except Archives from Encryption'] - CrossOrigin.json url, (response) -> - {media} = response - if media then for key of media when /_link$/.test key - # Image/thumbnail URLs loaded over HTTP can be modified in transit. - # Require them to be from a known HTTP host so that no referrer is sent to them from an HTTPS page. - delete media[key] unless media[key]? and media[key].match(/^(http:\/\/[^\/]+\/)?/)[0] in url.archive.imagehosts - Get.parseArchivedPost response, boardID, postID, root, context - return true - return false - parseArchivedPost: (data, boardID, postID, root, context) -> - # In case of multiple callbacks for the same request, - # don't parse the same original post more than once. - if post = g.posts["#{boardID}.#{postID}"] - Get.insert post, root, context - return - - if data.error - $.addClass root, 'warning' - root.textContent = data.error - return - - # https://github.com/eksopl/fuuka/blob/master/Board/Yotsuba.pm#L413-452 - # https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138 - comment = (data.comment or '').split /(\n|\[\/?(?:b|spoiler|code|moot|banned)\])/ - comment = for text, i in comment - if i % 2 is 1 - Get.archiveTags[text] - else - greentext = text[0] is '>' - text = text.replace /(\[\/?[a-z]+):lit(\])/, '$1$2' - text = for text2, j in text.split /(>>(?:>\/[a-z\d]+\/)?\d+)/g - if j % 2 is 1 - <%= html('${text2}') %> - else - <%= html('${text2}') %> - text = <%= html('@{text}') %> - text = <%= html('&{text}') %> if greentext - text - comment = <%= html('@{comment}') %> - - threadID = +data.thread_num - o = - # id - postID: postID - threadID: threadID - boardID: boardID - # info - name: data.name - capcode: switch data.capcode - when 'M' then 'mod' - when 'A' then 'admin' - when 'D' then 'developer' - tripcode: data.trip - uniqueID: data.poster_hash - email: data.email or '' - subject: data.title - flagCode: data.poster_country - flagName: data.poster_country_name - date: data.fourchan_date - dateUTC: data.timestamp - comment: comment - # file - if data.media?.media_filename - o.file = - name: data.media.media_filename - timestamp: data.media.media_orig - url: data.media.media_link or data.media.remote_media_link or - "//i.4cdn.org/#{boardID}/#{encodeURIComponent data.media[if boardID is 'f' then 'media_filename' else 'media_orig']}" - height: data.media.media_h - width: data.media.media_w - MD5: data.media.media_hash - size: data.media.media_size - turl: data.media.thumb_link or "//t.4cdn.org/#{boardID}/#{data.media.preview_orig}" - theight: data.media.preview_h - twidth: data.media.preview_w - isSpoiler: data.media.spoiler is '1' - o.file.tag = JSON.parse(data.media.exif).Tag if boardID is 'f' - - board = g.boards[boardID] or - new Board boardID - thread = g.threads["#{boardID}.#{threadID}"] or - new Thread threadID, board - post = new Post Build.post(o), thread, board, {isArchived: true} - post.file.thumbURL = o.file.turl if post.file - post.isFetchedQuote = true - Main.callbackNodes Post, [post] - Get.insert post, root, context - archiveTags: - '\n': <%= html('
') %> - '[b]': <%= html('') %> - '[/b]': <%= html('') %> - '[spoiler]': <%= html('') %> - '[/spoiler]': <%= html('') %> - '[code]': <%= html('
') %>
-    '[/code]':    <%= html('
') %> - '[moot]': <%= html('
') %> - '[/moot]': <%= html('
') %> - '[banned]': <%= html('') %> - '[/banned]': <%= html('') %> diff --git a/src/General/lib/classes.coffee b/src/General/lib/classes.coffee index eeb3095e5..e89bd4a04 100755 --- a/src/General/lib/classes.coffee +++ b/src/General/lib/classes.coffee @@ -10,3 +10,4 @@ <%= grunt.file.read('src/General/lib/simpledict.class') %> <%= grunt.file.read('src/General/lib/set.class') %> <%= grunt.file.read('src/General/lib/connection.class') %> +<%= grunt.file.read('src/General/lib/fetcher.class') %> diff --git a/src/General/lib/fetcher.class b/src/General/lib/fetcher.class new file mode 100644 index 000000000..a78aa0099 --- /dev/null +++ b/src/General/lib/fetcher.class @@ -0,0 +1,182 @@ +class Fetcher + constructor: (@boardID, @threadID, @postID, @root, @context) -> + if post = g.posts["#{@boardID}.#{@postID}"] + @insert post + return + + @root.textContent = "Loading post No.#{@postID}..." + if @threadID + $.cache "//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json", do (self = @) -> -> + self.fetchedPost @ + else + @archivedPost() + insert: (post) -> + # Stop here if the container has been removed while loading. + return unless @root.parentNode + clone = post.addClone @context, ($.hasClass @root, 'dialog') + Main.callbackNodes Clone, [clone] + + # Get rid of the side arrows. + {nodes} = clone + $.rmAll nodes.root + $.add nodes.root, nodes.post + + $.rmAll @root + $.add @root, nodes.root + $.event 'PostsInserted' + fetchedPost: (req) -> + # In case of multiple callbacks for the same request, + # don't parse the same original post more than once. + if post = g.posts["#{@boardID}.#{@postID}"] + @insert post + return + + {status} = req + unless status in [200, 304] + # The thread can die by the time we check a quote. + unless @archivedPost() + $.addClass @root, 'warning' + @root.textContent = + if status is 404 + "Thread No.#{@threadID} 404'd." + else + "Error #{req.statusText} (#{req.status})." + return + + {posts} = req.response + Build.spoilerRange[@boardID] = posts[0].custom_spoiler + for post in posts + break if post.no is @postID # we found it! + + if post.no isnt @postID + # Cached requests can be stale and must be rechecked. + if req.cached + api = "//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json" + $.cleanCache (url) -> url is api + $.cache api, do (self = @) -> -> + self.fetchedPost @ + return + # The post can be deleted by the time we check a quote. + unless @archivedPost() + $.addClass @root, 'warning' + @root.textContent = "Post No.#{@postID} was not found." + return + + board = g.boards[@boardID] or + new Board @boardID + thread = g.threads["#{@boardID}.#{@threadID}"] or + new Thread @threadID, board + post = new Post Build.postFromObject(post, @boardID), thread, board + post.isFetchedQuote = true + Main.callbackNodes Post, [post] + @insert post + archivedPost: -> + return false unless Conf['Resurrect Quotes'] + return false unless url = Redirect.to 'post', {@boardID, @postID} + if /^https:\/\//.test(url) or location.protocol is 'http:' + $.cache url, + do (self = @) -> -> self.parseArchivedPost @response + , + responseType: 'json' + withCredentials: url.archive.withCredentials + return true + else if Conf['Except Archives from Encryption'] + CrossOrigin.json url, (response) => + {media} = response + if media then for key of media when /_link$/.test key + # Image/thumbnail URLs loaded over HTTP can be modified in transit. + # Require them to be from a known HTTP host so that no referrer is sent to them from an HTTPS page. + delete media[key] unless media[key]? and media[key].match(/^(http:\/\/[^\/]+\/)?/)[0] in url.archive.imagehosts + @parseArchivedPost response + return true + return false + parseArchivedPost: (data) -> + # In case of multiple callbacks for the same request, + # don't parse the same original post more than once. + if post = g.posts["#{@boardID}.#{@postID}"] + @insert post + return + + if data.error + $.addClass @root, 'warning' + @root.textContent = data.error + return + + # https://github.com/eksopl/fuuka/blob/master/Board/Yotsuba.pm#L413-452 + # https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138 + comment = (data.comment or '').split /(\n|\[\/?(?:b|spoiler|code|moot|banned)\])/ + comment = for text, i in comment + if i % 2 is 1 + @archiveTags[text] + else + greentext = text[0] is '>' + text = text.replace /(\[\/?[a-z]+):lit(\])/, '$1$2' + text = for text2, j in text.split /(>>(?:>\/[a-z\d]+\/)?\d+)/g + if j % 2 is 1 + <%= html('${text2}') %> + else + <%= html('${text2}') %> + text = <%= html('@{text}') %> + text = <%= html('&{text}') %> if greentext + text + comment = <%= html('@{comment}') %> + + @threadID = +data.thread_num + o = + # id + postID: @postID + threadID: @threadID + boardID: @boardID + # info + name: data.name + capcode: switch data.capcode + when 'M' then 'mod' + when 'A' then 'admin' + when 'D' then 'developer' + tripcode: data.trip + uniqueID: data.poster_hash + email: data.email or '' + subject: data.title + flagCode: data.poster_country + flagName: data.poster_country_name + date: data.fourchan_date + dateUTC: data.timestamp + comment: comment + # file + if data.media?.media_filename + o.file = + name: data.media.media_filename + timestamp: data.media.media_orig + url: data.media.media_link or data.media.remote_media_link or + "//i.4cdn.org/#{@boardID}/#{encodeURIComponent data.media[if @boardID is 'f' then 'media_filename' else 'media_orig']}" + height: data.media.media_h + width: data.media.media_w + MD5: data.media.media_hash + size: data.media.media_size + turl: data.media.thumb_link or "//t.4cdn.org/#{@boardID}/#{data.media.preview_orig}" + theight: data.media.preview_h + twidth: data.media.preview_w + isSpoiler: data.media.spoiler is '1' + o.file.tag = JSON.parse(data.media.exif).Tag if @boardID is 'f' + + board = g.boards[@boardID] or + new Board @boardID + thread = g.threads["#{@boardID}.#{@threadID}"] or + new Thread @threadID, board + post = new Post Build.post(o), thread, board, {isArchived: true} + post.file.thumbURL = o.file.turl if post.file + post.isFetchedQuote = true + Main.callbackNodes Post, [post] + @insert post + archiveTags: + '\n': <%= html('
') %> + '[b]': <%= html('') %> + '[/b]': <%= html('') %> + '[spoiler]': <%= html('') %> + '[/spoiler]': <%= html('') %> + '[code]': <%= html('
') %>
+    '[/code]':    <%= html('
') %> + '[moot]': <%= html('
') %> + '[/moot]': <%= html('
') %> + '[banned]': <%= html('') %> + '[/banned]': <%= html('') %> diff --git a/src/Quotelinks/QuoteInline.coffee b/src/Quotelinks/QuoteInline.coffee index 3ae1634b3..26d6a553b 100755 --- a/src/Quotelinks/QuoteInline.coffee +++ b/src/Quotelinks/QuoteInline.coffee @@ -60,7 +60,7 @@ QuoteInline = qroot = $.x 'ancestor::*[contains(@class,"postContainer")][1]', root $.addClass qroot, 'hasInline' - Get.postClone boardID, threadID, postID, inline, context + new Fetcher boardID, threadID, postID, inline, context return unless (post = g.posts["#{boardID}.#{postID}"]) and context.thread is post.thread diff --git a/src/Quotelinks/QuotePreview.coffee b/src/Quotelinks/QuotePreview.coffee index 17c9e1b0c..ec5c75b99 100755 --- a/src/Quotelinks/QuotePreview.coffee +++ b/src/Quotelinks/QuotePreview.coffee @@ -24,7 +24,7 @@ QuotePreview = className: 'dialog' $.add Header.hover, qp - Get.postClone boardID, threadID, postID, qp, Get.contextFromNode @ + new Fetcher boardID, threadID, postID, qp, Get.contextFromNode @ UI.hover root: @