diff --git a/.gitignore b/.gitignore index 6cf9326..7796f16 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,10 @@ # logs geckodriver.log + +# Local build outputs +*.xpi +*.zip + +# Local captured exports +*.ndjson diff --git a/FORK-NOTICE.md b/FORK-NOTICE.md new file mode 100644 index 0000000..12f7ac8 --- /dev/null +++ b/FORK-NOTICE.md @@ -0,0 +1,20 @@ +# Pesquisa Social Fork Notice + +This project is a modified fork of Zeeschuimer. + +Original project: +- Zeeschuimer +- Copyright (c) Stijn Peeters +- Original repository: https://github.com/digitalmethodsinitiative/zeeschuimer + +This fork contains local modifications by Danilo F. Marinho and is distributed +with the obligations of the Mozilla Public License 2.0. + +MPL 2.0 compliance notes: +- Files derived from the original MPL-covered code remain under MPL 2.0. +- Original copyright and license notices must be preserved. +- If this extension is distributed in executable form, the corresponding source + code for the MPL-covered files must also be made available. + +Fork contact: +- danilo-f.marinho@hotmail.com diff --git a/README.md b/README.md index 85ff94c..8de4d1e 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,10 @@ be exported as a JSON file or exported to a [4CAT](https://github.com/digitalmet analysis and storage. Zeeschuimer is primarily intended as a companion to 4CAT, but you can also integrate its output into your own analysis pipeline. -Currently, it supports the following platforms: +This fork currently supports the following platforms: * [TikTok](https://www.tiktok.com) (posts and comments) -* [Instagram](https://www.instagram.com) (posts only) -* [X/Twitter](https://www.x.com) -* [LinkedIn](https://www.linkedin.com) -* [9gag](https://9gag.com) -* [Imgur](https://imgur.com) -* [Douyin](https://douyin.com) -* [Gab](https://gab.com) -* [Truth Social](https://truth.social) -* [Pinterest](https://pinterest.com) -* [RedNote/Xiaohongshu](https://xiaohongshu.com) +* [Instagram](https://www.instagram.com) (posts, reels, and comments) +* [X/Twitter](https://www.x.com) (posts and comments) Platform support requires regular maintenance to keep up with changes to the platforms. If something does not work, we welcome issues and pull requests. See 'Limitations' below for some known limitations to data capture. diff --git a/SOURCE-CODE-README.md b/SOURCE-CODE-README.md new file mode 100644 index 0000000..9898995 --- /dev/null +++ b/SOURCE-CODE-README.md @@ -0,0 +1,56 @@ +# Source Code Package for Mozilla Review + +This archive contains the source code used to build the Firefox extension +"Pesquisa Social". + +## Extension identity + +- Add-on name: Pesquisa Social +- Add-on ID: pesquisa-social@local +- Fork contact: danilo-f.marinho@hotmail.com + +## Origin and license + +This project is a modified fork of Zeeschuimer. + +- Original project: https://github.com/digitalmethodsinitiative/zeeschuimer +- Original copyright: Stijn Peeters +- License for MPL-covered files: Mozilla Public License 2.0 + +See these files in the source package: + +- `LICENSE` +- `FORK-NOTICE.md` + +## Build environment + +- Operating system used to produce the submitted package: Windows +- Shell used: PowerShell +- Archive/build script: `build-xpi.ps1` +- Required software: PowerShell with .NET support for `System.IO.Compression` + +No Node.js bundling, transpilation, or webpack build is required for this +extension package. + +## How to reproduce the submitted XPI + +1. Extract this source code package. +2. Open PowerShell in the project root. +3. Run: + +```powershell +powershell -ExecutionPolicy Bypass -File .\build-xpi.ps1 +``` + +4. The script creates: + +```text +pesquisa-social-v1.13.9.xpi +``` + +## Notes for reviewers + +- The package includes third-party assets already present in the repository, + such as Font Awesome and font files. +- The submitted extension package excludes local data exports and other build + artifacts such as `.ndjson`, `.xpi`, and `.zip` files. diff --git a/build-source-zip.ps1 b/build-source-zip.ps1 new file mode 100644 index 0000000..059dfb2 --- /dev/null +++ b/build-source-zip.ps1 @@ -0,0 +1,61 @@ +Add-Type -AssemblyName System.IO.Compression +Add-Type -AssemblyName System.IO.Compression.FileSystem + +$manifest = Get-Content "manifest.json" | ConvertFrom-Json +$version = $manifest.version +$output = "pesquisa-social-source-v$version.zip" +$root = (Get-Location).Path + +$includePaths = @( + "manifest.json", + "popup", + "js", + "modules", + "images", + "fonts", + "inc", + "LICENSE", + "README.md", + "FORK-NOTICE.md", + "SOURCE-CODE-README.md", + "build-xpi.ps1" +) + +if (Test-Path $output) { + Remove-Item -LiteralPath $output -Force +} + +$zip = [System.IO.Compression.ZipFile]::Open($output, [System.IO.Compression.ZipArchiveMode]::Create) + +try { + foreach ($path in $includePaths) { + $fullPath = Join-Path $root $path + + if (Test-Path $fullPath -PathType Leaf) { + [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile( + $zip, + $fullPath, + ($path -replace '\\', '/'), + [System.IO.Compression.CompressionLevel]::Optimal + ) | Out-Null + continue + } + + if (Test-Path $fullPath -PathType Container) { + Get-ChildItem -LiteralPath $fullPath -Recurse -File | ForEach-Object { + $entryName = $_.FullName.Substring($root.Length).TrimStart('\') -replace '\\', '/' + [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile( + $zip, + $_.FullName, + $entryName, + [System.IO.Compression.CompressionLevel]::Optimal + ) | Out-Null + } + } + } +} +finally { + $zip.Dispose() +} + +Write-Output "Created $output" diff --git a/build-xpi.ps1 b/build-xpi.ps1 new file mode 100644 index 0000000..82d3dba --- /dev/null +++ b/build-xpi.ps1 @@ -0,0 +1,73 @@ +$manifest = Get-Content "manifest.json" | ConvertFrom-Json +$name = ($manifest.name -replace '[^a-zA-Z0-9_-]+', '-').ToLower().Trim('-') +$version = $manifest.version +$output = "$name-v$version.xpi" + +Add-Type -AssemblyName System.IO.Compression +Add-Type -AssemblyName System.IO.Compression.FileSystem + +$root = (Get-Location).Path + +$excludeNames = @( + ".git", + ".github", + ".build-xpi", + "tests", + "create-zip.sh", + "create-zip-bash.sh", + "build-xpi.ps1" +) + +$excludePatterns = @( + "*.zip", + "*.xpi", + "*.DS_Store", + "*.ndjson" +) + +function Should-Skip($item) { + foreach ($name in $excludeNames) { + if ($item.Name -eq $name) { + return $true + } + } + + foreach ($pattern in $excludePatterns) { + if ($item.Name -like $pattern) { + return $true + } + } + + return $false +} + +if (Test-Path $output) { + Remove-Item -LiteralPath $output -Force +} + +$zip = [System.IO.Compression.ZipFile]::Open($output, [System.IO.Compression.ZipArchiveMode]::Create) + +try { + Get-ChildItem -LiteralPath $root -Recurse -File | ForEach-Object { + $file = $_ + + $segments = $file.FullName.Substring($root.Length).TrimStart('\').Split('\') + foreach ($segment in $segments) { + if ($excludeNames -contains $segment) { + return + } + } + + if (Should-Skip $file) { + return + } + + $entryName = ($file.FullName.Substring($root.Length).TrimStart('\')) -replace '\\', '/' + [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $file.FullName, $entryName, [System.IO.Compression.CompressionLevel]::Optimal) | Out-Null + } +} +finally { + $zip.Dispose() +} + +Write-Output "Created $output" diff --git a/js/zs-background.js b/js/zs-background.js index 10880f7..c7c7db9 100644 --- a/js/zs-background.js +++ b/js/zs-background.js @@ -344,8 +344,6 @@ window.zeeschuimer = { } }); })); - - return; } } }, diff --git a/manifest.json b/manifest.json index b598fa0..df60eaa 100644 --- a/manifest.json +++ b/manifest.json @@ -1,14 +1,14 @@ { - "description": "Collect data while browsing social media platforms and upload it for analysis later", + "description": "Capture social media data while browsing and export it for later analysis", "manifest_version": 2, - "name": "Zeeschuimer", - "version": "1.13.6", - "homepage_url": "https://github.com/digitalmethodsinitiative/zeeschuimer", + "name": "Pesquisa Social", + "version": "1.13.9", + "homepage_url": "mailto:danilo-f.marinho@hotmail.com", "browser_specific_settings": { "gecko": { - "update_url": "https://extensions.digitalmethods.net/updates.json", + "id": "pesquisa-social@local", "data_collection_permissions": { "required": ["none"] } @@ -22,7 +22,7 @@ "browser_action": { "default_icon": "images/zeeschuimer-64.png", - "default_title": "Zeeschuimer Status" + "default_title": "Pesquisa Social" }, "permissions": [ @@ -41,17 +41,9 @@ "modules/tiktok.js", "modules/tiktok-comments.js", "modules/instagram.js", - "modules/linkedin.js", - "modules/9gag.js", - "modules/imgur.js", + "modules/instagram-comments.js", "modules/twitter.js", - "modules/douyin.js", - "modules/gab.js", - "modules/truth.js", - "modules/threads.js", - "modules/pinterest.js", - "modules/rednote.js", - "modules/rednote-comments.js" + "modules/twitter-comments.js" ] } } diff --git a/modules/instagram-comments.js b/modules/instagram-comments.js new file mode 100644 index 0000000..bae0cdd --- /dev/null +++ b/modules/instagram-comments.js @@ -0,0 +1,141 @@ +zeeschuimer.register_module( + 'Instagram (comments)', + 'instagram.com', + function (response, source_platform_url, source_url) { + let domain = source_platform_url.split("/")[2].toLowerCase().replace(/^www\./, ''); + + if (!["instagram.com"].includes(domain)) { + return []; + } + + const lower_source_url = source_url.toLowerCase(); + const looks_like_comments_request = lower_source_url.indexOf('/comments') >= 0 + || lower_source_url.indexOf('comments') >= 0 + || lower_source_url.indexOf('comment') >= 0; + + if (!looks_like_comments_request) { + return []; + } + + let data; + try { + if (response.startsWith("for (;;);")) { + response = response.slice("for (;;);".length); + } + data = JSON.parse(response); + } catch (SyntaxError) { + return []; + } + + const media_id_match = source_url.match(/\/media\/([^\/?]+)\/comments/i); + const shortcode_match = source_platform_url.match(/\/(p|reel|reels)\/([^\/?#]+)/i); + const post_id_from_url = media_id_match ? media_id_match[1] : null; + const post_type = shortcode_match ? shortcode_match[1].replace('reels', 'reel') : 'p'; + const post_shortcode = shortcode_match ? shortcode_match[2] : null; + const post_url = post_shortcode ? 'https://www.instagram.com/' + post_type + '/' + post_shortcode + '/' : source_platform_url; + + let comments = []; + let seen = new Set(); + + const normalise_user = function (user) { + if (!user) { + return null; + } + + const user_id = user['pk'] || user['pk_id'] || user['id']; + + return { + id: user_id ? String(user_id) : undefined, + unique_id: user['username'], + nickname: user['full_name'], + avatar_thumb: user['profile_pic_url'], + verified: !!user['is_verified'], + is_private: !!user['is_private'] + }; + }; + + const add_comment = function (comment, parent_comment_id=null) { + if (!comment || typeof comment !== "object") { + return; + } + + const comment_id = comment['pk'] || comment['id']; + const text = comment['text']; + const user = normalise_user(comment['user'] || comment['owner']); + const post_id = comment['media_id'] || comment['media_pk'] || post_id_from_url; + + if (!comment_id || !text || !user || !post_id) { + return; + } + + if (seen.has(String(comment_id))) { + return; + } + seen.add(String(comment_id)); + + comment['id'] = String(comment_id); + comment['comment_id'] = String(comment_id); + comment['text'] = text; + comment['user'] = user; + comment['post_id'] = String(post_id); + comment['post_shortcode'] = post_shortcode; + comment['post_url'] = post_url; + comment['parent_comment_id'] = parent_comment_id ? String(parent_comment_id) : null; + comment['thread_id'] = String(post_id); + comment['_zs_comment_parent_id'] = parent_comment_id ? String(parent_comment_id) : String(post_id); + comment['_zs_comment_thread_id'] = String(post_id); + comment['_zs_comment_post_id'] = String(post_id); + + comments.push(comment); + }; + + const traverse = function (obj, parent_comment_id=null) { + if (!obj || typeof obj !== "object") { + return; + } + + if (Array.isArray(obj)) { + for (const item of obj) { + traverse(item, parent_comment_id); + } + return; + } + + if ((obj['pk'] || obj['id']) && obj['text'] && (obj['user'] || obj['owner'])) { + add_comment(obj, parent_comment_id); + + const comment_id = obj['pk'] || obj['id']; + for (const replies_key of ['child_comments', 'preview_child_comments', 'inline_child_comments']) { + if (Array.isArray(obj[replies_key])) { + for (const reply of obj[replies_key]) { + traverse(reply, comment_id); + } + } + } + return; + } + + for (let property in obj) { + if (!obj.hasOwnProperty(property) || !obj[property]) { + continue; + } + + if (property === 'comments' && Array.isArray(obj[property])) { + for (const comment of obj[property]) { + traverse(comment, parent_comment_id); + } + } else if (property === 'edges' && Array.isArray(obj[property])) { + for (const edge of obj[property]) { + traverse(edge && edge['node'] ? edge['node'] : edge, parent_comment_id); + } + } else if (typeof obj[property] === "object") { + traverse(obj[property], parent_comment_id); + } + } + }; + + traverse(data); + return comments; + }, + 'instagram-comments' +); diff --git a/modules/twitter-comments.js b/modules/twitter-comments.js new file mode 100644 index 0000000..e3915ad --- /dev/null +++ b/modules/twitter-comments.js @@ -0,0 +1,264 @@ +zeeschuimer.register_module( + 'X/Twitter (comments)', + 'x.com', + function (response, source_platform_url, source_url) { + let domain = source_platform_url.split("/")[2].toLowerCase().replace(/^www\./, ''); + const root_tweet_id_match = source_platform_url.match(/\/status\/(\d+)/); + const root_tweet_id = root_tweet_id_match ? root_tweet_id_match[1] : null; + const looks_like_tweet_request = source_url.indexOf('TweetDetail') >= 0 + || source_url.indexOf('tweetdetail') >= 0 + || source_url.indexOf('/graphql/') >= 0 + || source_url.indexOf('/i/api/') >= 0; + + if (!["x.com"].includes(domain)) { + return []; + } + + // X frequently changes the exact GraphQL operation name used to load + // replies. When the user is on a /status/... page, be permissive about + // which request may contain the thread payload. + if (!root_tweet_id && !looks_like_tweet_request) { + return []; + } + + let data; + try { + data = JSON.parse(response); + } catch (SyntaxError) { + return []; + } + + let comments = []; + let seen = new Set(); + + const normalise_tweet = function (tweet) { + if (!tweet || tweet['__typename'] === 'TweetUnavailable') { + return null; + } + + if ('tweet' in tweet) { + tweet = tweet['tweet']; + } + + if (!tweet['legacy']) { + return null; + } + + const tweet_id = tweet['legacy']['id_str'] || tweet['rest_id']; + if (!tweet_id) { + return null; + } + + tweet['id'] = tweet_id; + return tweet; + }; + + const normalise_user = function (tweet) { + const user = tweet['core'] && tweet['core']['user_results'] && tweet['core']['user_results']['result']; + if (user) { + const core = user['core'] || {}; + const legacy = user['legacy'] || {}; + const avatar = user['avatar'] || {}; + + return { + id: user['rest_id'] || user['id'], + unique_id: core['screen_name'], + nickname: core['name'], + signature: legacy['description'], + avatar_thumb: avatar['image_url'] || legacy['profile_image_url_https'], + verified: !!(user['verification'] && user['verification']['verified']), + verified_type: user['verification'] ? user['verification']['verified_type'] : undefined, + follower_count: legacy['followers_count'], + following_count: legacy['friends_count'] + }; + } + + const fallback_user = tweet['user'] || null; + if (!fallback_user) { + return null; + } + + return { + id: fallback_user['id_str'] || fallback_user['id'], + unique_id: fallback_user['screen_name'], + nickname: fallback_user['name'], + signature: fallback_user['description'], + avatar_thumb: fallback_user['profile_image_url_https'], + verified: !!fallback_user['verified'], + follower_count: fallback_user['followers_count'], + following_count: fallback_user['friends_count'] + }; + }; + + const is_comment = function (tweet) { + const legacy = tweet['legacy']; + const tweet_id = String(tweet['id']); + const conversation_id = legacy['conversation_id_str'] ? String(legacy['conversation_id_str']) : null; + const reply_to_id = legacy['in_reply_to_status_id_str'] ? String(legacy['in_reply_to_status_id_str']) : null; + + if (root_tweet_id) { + return tweet_id !== root_tweet_id + && (conversation_id === root_tweet_id || reply_to_id === root_tweet_id); + } + + return !!reply_to_id; + }; + + const add_tweet = function (tweet) { + tweet = normalise_tweet(tweet); + if (!tweet || !is_comment(tweet)) { + return; + } + + const tweet_id = String(tweet['id']); + if (seen.has(tweet_id)) { + return; + } + seen.add(tweet_id); + + const parent_id = tweet['legacy']['in_reply_to_status_id_str'] || null; + const post_id = root_tweet_id || tweet['legacy']['conversation_id_str'] || parent_id; + const author = normalise_user(tweet); + + // Keep the original Twitter/X payload, but expose TikTok-like fields + // for downstream analysis pipelines that need text, author and post id. + tweet['comment_id'] = tweet_id; + tweet['text'] = tweet['legacy']['full_text'] || tweet['legacy']['text'] || ''; + tweet['user'] = author; + tweet['post_id'] = post_id; + tweet['post_url'] = post_id ? 'https://x.com/i/web/status/' + post_id : null; + tweet['parent_comment_id'] = parent_id && parent_id !== post_id ? parent_id : null; + tweet['thread_id'] = tweet['legacy']['conversation_id_str'] || post_id; + tweet['_zs_comment_parent_id'] = parent_id || post_id; + tweet['_zs_comment_thread_id'] = post_id; + tweet['_zs_comment_post_id'] = post_id; + comments.push(tweet); + }; + + const add_from_item_content = function (item_content) { + if (!item_content || !item_content['tweet_results']) { + return; + } + + add_tweet(item_content['tweet_results']['result']); + }; + + const collect_tweet_candidates = function (obj) { + if (!obj || typeof obj !== "object") { + return; + } + + if (Array.isArray(obj)) { + for (const item of obj) { + collect_tweet_candidates(item); + } + return; + } + + // Direct tweet-like object + if ( + obj['legacy'] + && typeof obj['legacy'] === 'object' + && (obj['rest_id'] || obj['id'] || obj['legacy']['id_str']) + ) { + add_tweet(obj); + } + + // Common GraphQL wrapper + if (obj['tweet_results'] && obj['tweet_results']['result']) { + add_tweet(obj['tweet_results']['result']); + } + + // Some responses wrap the tweet one level deeper + if (obj['result'] && typeof obj['result'] === 'object') { + const result = obj['result']; + if ( + result['legacy'] + || result['tweet'] + || result['tweet_results'] + || result['__typename'] === 'Tweet' + || result['__typename'] === 'TweetWithVisibilityResults' + ) { + collect_tweet_candidates(result); + } + } + + if (obj['tweet'] && typeof obj['tweet'] === 'object') { + collect_tweet_candidates(obj['tweet']); + } + + for (let property in obj) { + if (!obj.hasOwnProperty(property) || !obj[property]) { + continue; + } + + if (typeof obj[property] === "object") { + collect_tweet_candidates(obj[property]); + } + } + }; + + const traverse = function (obj) { + for (let property in obj) { + let child = obj[property]; + if (!child) { + continue; + } + + if ( + ( + (child.hasOwnProperty('type') && child['type'] === 'TimelineAddEntries') + || (!child.hasOwnProperty('type') && Object.keys(child).length === 1) + ) + && child.hasOwnProperty('entries') + ) { + for (let entry in child['entries']) { + entry = child['entries'][entry]; + if (!entry['content']) { + continue; + } + + if ('itemContent' in entry['content']) { + add_from_item_content(entry['content']['itemContent']); + } else if (entry['content']['content'] && entry['content']['content']['itemContent']) { + add_from_item_content(entry['content']['content']['itemContent']); + } else if ('__typename' in entry['content'] && entry['content']['__typename'] === 'TimelineTimelineModule') { + for (const item of entry['content']['items']) { + const item_content = item['item'] && item['item']['itemContent']; + if (!item_content || !['Tweet', 'TimelineTweet'].includes(item_content['__typename'])) { + continue; + } + + add_from_item_content(item_content); + } + } else if (entry['entryId'] && data['globalObjects'] && data['globalObjects']['tweets']) { + let tweet_id = null; + if (entry['entryId'].indexOf('tweet-') === 0) { + tweet_id = entry['entryId'].split('-')[1]; + } else if (entry['entryId'].indexOf('sq-I-t-') === 0) { + tweet_id = entry['entryId'].split('-')[3]; + } + + if (tweet_id && data['globalObjects']['tweets'][tweet_id]) { + add_tweet({ + id: tweet_id, + legacy: data['globalObjects']['tweets'][tweet_id], + user: data['globalObjects']['users'] + ? data['globalObjects']['users'][data['globalObjects']['tweets'][tweet_id]['user_id_str']] + : null + }); + } + } + } + } else if (typeof (child) === "object") { + traverse(child); + } + } + }; + + traverse(data); + collect_tweet_candidates(data); + return comments; + }, + 'twitter-comments' +); diff --git a/modules/twitter.js b/modules/twitter.js index 0a36282..66e1301 100644 --- a/modules/twitter.js +++ b/modules/twitter.js @@ -3,6 +3,8 @@ zeeschuimer.register_module( 'x.com', function (response, source_platform_url, source_url) { let domain = source_platform_url.split("/")[2].toLowerCase().replace(/^www\./, ''); + const root_tweet_id_match = source_platform_url.match(/\/status\/(\d+)/); + const root_tweet_id = root_tweet_id_match ? root_tweet_id_match[1] : null; if ( !["x.com"].includes(domain) @@ -37,6 +39,51 @@ zeeschuimer.register_module( // One of the 'instructions' is to add entries to the timeline, this is what we are interested in because what // is added to the timeline are the tweets! // So find those instructions in the object, and reconstruct the tweets from there + const normalise_tweet = function (tweet) { + if (!tweet || tweet['__typename'] === 'TweetUnavailable') { + return null; + } + + if ('tweet' in tweet) { + tweet = tweet['tweet']; + } + + if (!tweet['legacy']) { + return null; + } + + tweet['id'] = tweet['legacy']['id_str'] || tweet['rest_id']; + return tweet['id'] ? tweet : null; + }; + + const should_include_as_post = function (tweet) { + tweet = normalise_tweet(tweet); + if (!tweet) { + return false; + } + + if (!root_tweet_id) { + return true; + } + + const tweet_id = String(tweet['id']); + const legacy = tweet['legacy'] || {}; + const conversation_id = legacy['conversation_id_str'] ? String(legacy['conversation_id_str']) : null; + const reply_to_id = legacy['in_reply_to_status_id_str'] ? String(legacy['in_reply_to_status_id_str']) : null; + + // On a single-tweet thread page, keep the root post in the posts + // stream and leave replies to the dedicated comments module. + if (tweet_id === root_tweet_id) { + return true; + } + + if (conversation_id === root_tweet_id || reply_to_id === root_tweet_id || !!reply_to_id) { + return false; + } + + return true; + }; + let traverse = function (obj) { for (let property in obj) { let child = obj[property]; @@ -69,11 +116,11 @@ zeeschuimer.register_module( continue; } - if('tweet' in tweet) { - // sometimes this is nested once more, for some reason - tweet = tweet['tweet']; + tweet = normalise_tweet(tweet); + if (!tweet || !should_include_as_post(tweet)) { + continue; } - tweet['id'] = tweet['legacy']['id_str']; + // distinguish tweets that were included because they were "promoted" from // those that are actually part of the user/home timeline or search result. // assume a tweet was promoted if itemContent has promotedMetadata @@ -87,7 +134,12 @@ zeeschuimer.register_module( }).map(item => { return item['item']['itemContent']['tweet_results']['result'] })) { - tweets.push({...reply_tweet, id: parseInt(reply_tweet['rest_id'])}); + const tweet = normalise_tweet(reply_tweet); + if (!tweet || !should_include_as_post(tweet)) { + continue; + } + + tweets.push(tweet); } } else { // in other cases this object only contains a reference to the full tweet, which is in turn @@ -116,7 +168,9 @@ zeeschuimer.register_module( // the user is also stored as a reference - so add the user data to the tweet tweet['user'] = data['globalObjects']['users'][tweet['legacy']['user_id_str']] - tweets.push(tweet); + if (should_include_as_post(tweet)) { + tweets.push(tweet); + } } } @@ -130,4 +184,4 @@ zeeschuimer.register_module( return tweets; }, 'twitter.com' -); \ No newline at end of file +); diff --git a/popup/interface.html b/popup/interface.html index 356f2b5..ce5e8cd 100644 --- a/popup/interface.html +++ b/popup/interface.html @@ -1,6 +1,6 @@
-