4chan-x/src/Filtering/Filter.coffee

305 lines
9.2 KiB
CoffeeScript

Filter =
filters: {}
results: {}
init: ->
return unless g.VIEW in ['index', 'thread'] and Conf['Filter']
unless Conf['Filtered Backlinks']
$.addClass doc, 'hide-backlinks'
nsfwBoards = BoardConfig.sfwBoards(false).join(',')
sfwBoards = BoardConfig.sfwBoards(true).join(',')
for key of Config.filter
for line in Conf[key].split '\n'
continue if line[0] is '#'
if not (regexp = line.match /\/(.*)\/(\w*)/)
continue
# Don't mix up filter flags with the regular expression.
filter = line.replace regexp[0], ''
# Comma-separated list of the boards this filter applies to.
# Defaults to global.
boardsRaw = filter.match(/boards:([^;]+)/)?[1].toLowerCase()
if boardsRaw
boards = {}
for board in boardsRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',')
boards[board] = true
else
boards = false
# boards to exclude from an otherwise global rule
# due to the sfw and nsfw keywords, also works on all filters
# replaces 'nsfw' and 'sfw' for consistency
excludesRaw = filter.match(/exclude:([^;]+)/)?[1].toLowerCase()
if excludesRaw
excludes = {}
for board in excludesRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',')
excludes[board] = true
else
excludes = false
if (isstring = (key in ['uniqueID', 'MD5']))
# MD5 filter will use strings instead of regular expressions.
regexp = regexp[1]
else
try
# Please, don't write silly regular expressions.
regexp = RegExp regexp[1], regexp[2]
catch err
# I warned you, bro.
new Notice 'warning', [
$.tn "Invalid #{key} filter:"
$.el 'br'
$.tn line
$.el 'br'
$.tn err.message
], 60
continue
# Filter OPs along with their threads or replies only.
op = filter.match(/[^t]op:(no|only)/)?[1] or ''
mask = {'no': 1, 'only': 2}[op] or 0
# Filter only posts with/without files.
file = filter.match(/file:(no|only)/)?[1] or ''
mask = mask | ({'no': 4, 'only': 8}[file] or 0)
# Overrule the `Show Stubs` setting.
# Defaults to stub showing.
stub = switch filter.match(/stub:(yes|no)/)?[1]
when 'yes'
true
when 'no'
false
else
Conf['Stubs']
# Desktop notification
noti = /notify/.test filter
# Highlight the post.
# If not specified, the highlight class will be filter-highlight.
if (hl = /highlight/.test filter)
hl = filter.match(/highlight:([\w-]+)/)?[1] or 'filter-highlight'
# Put highlighted OP's thread on top of the board page or not.
# Defaults to on top.
top = filter.match(/top:(yes|no)/)?[1] or 'yes'
top = top is 'yes' # Turn it into a boolean
# Fields that this filter applies to (for 'general' filters)
if key is 'general'
if (types = filter.match /(?:^|;)\s*type:([^;]*)/)
types = types[1].split(',').filter (x) ->
x of Config.filter and x isnt 'general'
else
types = ['subject', 'name', 'filename', 'comment']
# Hide the post (default case).
hide = !(hl or noti)
filter = {isstring, regexp, boards, excludes, mask, hide, stub, hl, top, noti}
if key is 'general'
for type in types
(@filters[type] or= []).push filter
else
(@filters[key] or= []).push filter
return unless Object.keys(@filters).length
Callbacks.Post.push
name: 'Filter'
cb: @node
test: (post, hideable=true) ->
return post.filterResults if post.filterResults
hide = false
stub = true
hl = undefined
top = false
noti = false
if QuoteYou.isYou(post)
hideable = false
mask = (if post.isReply then 2 else 1)
mask = (mask | (if post.file then 4 else 8))
for key of Filter.filters when ((value = Filter.value key, post)?)
# Continue if there's nothing to filter (no tripcode for example).
for filter in Filter.filters[key]
continue if (
(filter.boards and !filter.boards[post.boardID]) or
(filter.excludes and filter.excludes[post.boardID]) or
(filter.mask & mask) or
(if filter.isstring then (filter.regexp isnt value) else !filter.regexp.test(value))
)
if filter.hide
if hideable
hide = true
stub and= filter.stub
else
unless hl and filter.hl in hl
(hl or= []).push filter.hl
top or= filter.top
if filter.noti
noti = true
if hide
{hide, stub}
else
{hl, top, noti}
node: ->
return if @isClone
{hide, stub, hl, top, noti} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index'))
if hide
if @isReply
PostHiding.hide @, stub
else
ThreadHiding.hide @thread, stub
else
if hl
@highlights = hl
$.addClass @nodes.root, hl...
if noti and Unread.posts and (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@)
Unread.openNotification @, ' triggered a notification filter'
isHidden: (post) ->
!!Filter.test(post).hide
valueF:
postID: (post) -> "#{post.ID}"
name: (post) -> post.info.name
uniqueID: (post) -> post.info.uniqueID or ''
tripcode: (post) -> post.info.tripcode
capcode: (post) -> post.info.capcode
pass: (post) -> post.info.pass
subject: (post) -> post.info.subject or (if post.isReply then undefined else '')
comment: (post) -> (post.info.comment ?= Build.parseComment post.info.commentHTML.innerHTML)
flag: (post) -> post.info.flag
filename: (post) -> post.file?.name
dimensions: (post) -> post.file?.dimensions
filesize: (post) -> post.file?.size
MD5: (post) -> post.file?.MD5
value: (key, post) ->
Filter.valueF[key](post)
addFilter: (type, re, cb) ->
$.get type, Conf[type], (item) ->
save = item[type]
# Add a new line before the regexp unless the text is empty.
save =
if save
"#{save}\n#{re}"
else
re
$.set type, save, cb
quickFilterMD5: ->
post = Get.postFromNode @
return unless post.file
Filter.addFilter 'MD5', "/#{post.file.MD5}/"
origin = post.origin or post
if origin.isReply
PostHiding.hide origin
else if g.VIEW is 'index'
ThreadHiding.hide origin.thread
# If post is still visible, give an indication that the MD5 was filtered.
if post.nodes.post.getBoundingClientRect().height
new Notice 'info', 'MD5 filtered.', 2
escape: (value) ->
value.replace ///
/
| \\
| \^
| \$
| \n
| \.
| \(
| \)
| \{
| \}
| \[
| \]
| \?
| \*
| \+
| \|
///g, (c) ->
if c is '\n'
'\\n'
else if c is '\\'
'\\\\'
else
"\\#{c}"
menu:
init: ->
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Filter']
div = $.el 'div',
textContent: 'Filter'
entry =
el: div
order: 50
open: (post) ->
Filter.menu.post = post
true
subEntries: []
for type in [
['Name', 'name']
['Unique ID', 'uniqueID']
['Tripcode', 'tripcode']
['Capcode', 'capcode']
['Pass Date', 'pass']
['Subject', 'subject']
['Comment', 'comment']
['Flag', 'flag']
['Filename', 'filename']
['Image dimensions', 'dimensions']
['Filesize', 'filesize']
['Image MD5', 'MD5']
]
# Add a sub entry for each filter type.
entry.subEntries.push Filter.menu.createSubEntry type[0], type[1]
Menu.menu.addEntry entry
createSubEntry: (text, type) ->
el = $.el 'a',
href: 'javascript:;'
textContent: text
el.dataset.type = type
$.on el, 'click', Filter.menu.makeFilter
return {
el: el
open: (post) ->
value = Filter.value type, post
value?
}
makeFilter: ->
{type} = @dataset
# Convert value -> regexp, unless type is MD5
value = Filter.value type, Filter.menu.post
re = if type in ['uniqueID', 'MD5'] then value else Filter.escape(value)
re = if type in ['uniqueID', 'MD5']
"/#{re}/"
else
"/^#{re}$/"
Filter.addFilter type, re, ->
# Open the settings and display & focus the relevant filter textarea.
Settings.open 'Filter'
section = $ '.section-container'
select = $ 'select[name=filter]', section
select.value = type
Settings.selectFilter.call select
$.onExists section, 'textarea', (ta) ->
tl = ta.textLength
ta.setSelectionRange tl, tl
ta.focus()