# Project export: Banana Bulletin

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2024
- Tagline: Modern news creates an echo chamber where those who try to be informed end up reliant on one news source. The Banana Bulletin aims to broaden perspectives by showing related articles on other sources.
- Devpost: https://devpost.com/software/the-banana-bulletin
- GitHub: https://github.com/JMack21/cruzhacks24
- Result: winner (Justice Hacks)
- Team: 5 GitHub contributor(s) — fjordimm (38 commits), JMack21 (3 commits), arnav_vishwakarma (3 commits), arnav-v1 (2 commits), Navodit1603 (1 commits)

## Devpost submission (written by the team)

### Inspiration

Our inspiration for this came from the narrow perspective that people have on reading the news. Most people (us included), whether intentional or not, default to the same news source every time. For many this is either a force of habit, a strong preference, or a heavy political bias. Many Americans also exclusively view American news sources, even when viewing international events. Our goal for this project was to make a convenient way for our users to broaden their perspectives by being given a list of similar articles published by other news publications.

### What it does

Our program is a Google Chrome extension, that, when opened, reads the title of the current page, and then searches for similar results on other news sources. These results are then read out for users to click on to be redirected.

### How we built it

We built this program using HTML, CSS, and Javascript with Node.js, Axios, Cheerio, and Browserify. Our program runs fully in the browser. It uses Axios to request HTML data from the current website and Cheerio is used to parse that data. This data is used to form new URLs which direct to searches for the title of the current news article in other news sources. These responses are then also read with Cheerio to generate a list of links for the user.

### Challenges we ran into

We ran into many challenges as this was our first attempt at creating a browser extension beyond the most basic parts and also our first time using Node.js, Axios, Cheerio, npm, and Browserify. One of our largest problems was requesting data from searches on websites. Much of the data on websites cannot be read with just the basic HTML from a simple request. Instead, Javascript code has to receive data from the website’s backend, but these can't be run in an extension. The first ideas we had for this were to use packages like puppeteer and Selenium but they also wouldn't work in a browser extension. The next idea was to create an API backend server to run these browser pages and send data back to the browser through GET requests. This would work but would take a long time to program and would run slower then our final idea. This idea was to request data from a search engine instead of directly from a news source. This idea was very effective and didn't require us to host a server (currently it would have to be locally) and could instead be done fully in the browser extension's Javascript code.

### Accomplishments we're proud of

First and foremost we are proud of making a functional minimum viable product for our idea. We participated as a group in the previous CruzHacks and were way over our heads with our idea and couldn't create a product we felt satisfied with. This year our product fulfilled all the necessities we wanted met and even some of our stretch goals. It also looks visually appealing and is easy enough to use. We also came out of this event much more proficient in tools and languages we previously haven't touched.

### What we learned

This project taught all of us many valuable skills. We learned how to create stylings with CSS, how to make and await asynchronous HTML requests with Axios, how to create a full Javascript project with Node.js, how HTML is laid out and how to parse it with Cheerios, how to install and manage packages with npm, and some basics of how backend development works.

### What's next

for the Banana Bulletin All four of us are genuinely interested in continuing development of this project and are thinking of next steps. At the end of the day this is a hackathon project and many shortcuts were taken that should be implemented in better ways that we didn’t have time for. One of the larger things that needs some improvement for this project is how we request HTML data. The GET requests have to be followed by a timer so we don’t send too many requests as Axios only allows so many requests in a given time so we ideally should switch to some other way to request page information. There are also many features we didn’t have the time to add or complete. Some of these include options for users such as only showing news sources that don’t require subscriptions, or allowing users to choose which news sources to search. We are also interested in adding support for an LLM to help summarize and compare articles.

## README (from the GitHub repository)

# Banana Bulletin 
## _Broadened perspectives on news_

Banana Bulletin is a browser-extension that, upon visiting a news article, recommends users a broad range of articles from *other* publications on the same topic. It provides statistics on each recommended publication's political learning, and additionally recommends region-specific news articles. The intent is to broaden user's perspectives, and be more aware of the political bias that is present in their consumption of news. 
# [Product Pitch](https://docs.google.com/presentation/d/1RwCTT6aOG-9v8TgdM5efBjtgP5vhDCHcF1eZqiKp4Sw/edit?usp=sharing)
## Developers
* [Jason Mack](https://www.linkedin.com/in/jason-mack-89308225b/)
* [Bill Mularski](https://www.linkedin.com/in/william-mularski-597a71245/) 
* [Navodit Maheshwari](https://www.linkedin.com/in/navodit-maheshwari/)
* [Arnav Vishwakarma](https://www.linkedin.com/in/arnav-vishwakarma/)

![image](https://github.com/JMack21/cruzhacks24/assets/146882486/0db18ca7-3e7e-49af-a65d-2e9f13ef27d5)

## Future Features
- Options to toggle paywall only suggestions 
- Options for user to input *only* publications that they enter 
- Utilize AI to summarize other articles, compare writing style and biases 
- Statistics! Give users statistics to see how many articles from *other* publications they clicked, to see how they’ve broadened their perspective 
    - Make global statistics so people can see which sources and articles are more popular before clicking
- Slider for how often the sources they’ve looked at are left/centrist/right, and slider provides a point that describes where their news sources average 

## Developer Tools
Banana Bulletin utilizes the following frameworks: 

- [NodeJS] — Primary framework for interacting with web elements through Javascript 
- [Browserify]  — Package to allow for NodeJS features to run in an extension environment
- [Axios] — NodeJS package to get a page's source HTML 
- [Cheerio] — NodeJS package to parse a page's source HTML and extract relevant information
- [CSS] — Utilized for styling front-end elements


## Detected evidence (automated analysis)

Indexed codebase: 15 recognized source files, 911 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (22 of 22)

```
browserifybundles/bundle.js
images/bias_centrist.ai
images/bias_far_left.ai
images/bias_far_right.ai
images/bias_slight_left.ai
images/bias_slight_right.ai
index.html
mainscript.js
manifest.json
package.json
README.md
scripts/ui/exampleAddingSites.js
scripts/ui/mainNewsSites.js
scripts/ui/uistuff.js
scripts/webscraping/articlestuff.js
scripts/webscraping/country.js
scripts/webscraping/findname.js
scripts/webscraping/googlesearch.js
scripts/webscraping/googlesearching.js
scripts/webscraping/nyt.js
scripts/webscraping/stance.js
styles/styles.css
```

### Dependencies

- package.json: axios@^1.6.5, browserify@^17.0.0, cheerio@^1.0.0-rc.10, esmify@^2.1.1, puppeteer@21.7.0

### Recent commits (newest first)

- README updated with pitch
- README 2.0
- Merge branch 'main' into fixing-search-but-bill
- stuff works, this should be final version
- small changes
- made current article display related articles
- made current article display bias
- Update README.md
- changed header
- undid the repetitive thingy whatever
- made it do it
- thingie
- Merge branch 'main' into navodit-updatedfuncs
- added newline
- added another timeout guard
- changes if parameter is undefined
- added delays for api requests
- testing timing delay
- added another site for testing
- almost got article loading working

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
	"name": "banana-bulletin",
	"version": "0.1.0",
	"description": "Placeholder description",
	"author": "PlaceholderAuthor",
	"license": "PlaceholderLicense",
	"main": "mainscript.js",
	"scripts": {
		"build": "browserify mainscript.js -p esmify > browserifybundles/bundle.js"
	},
	"dependencies": {
		"axios": "^1.6.5",
		"cheerio": "^1.0.0-rc.10",
		"puppeteer": "21.7.0"
	},
	"devDependencies": {
		"browserify": "^17.0.0",
		"esmify": "^2.1.1"
	}
}

```

### index.html

```html
<!DOCTYPE html>

<html lang="en">

	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>CruzHacks24 Project</title>
		<link rel="stylesheet" href="./styles/styles.css">
	</head>

	<body>
		<div class="maindiv">
			<div class="main-vert-div">

				<div class="navbar-div">
					<h1>Banana Bulletin</h1>
				</div>

				<div class="list-of-newsites-div" id="the-list-of-newsites">
				</div>

			</div>
		</div>

		<script src="browserifybundles/bundle.js" charset="utf-8"></script>
	</body>

</html>
```

### mainscript.js

```javascript

const mainNewsSites = require('./scripts/ui/mainNewsSites');
const articlestuff = require('./scripts/webscraping/articlestuff');
const googlesearching = require('./scripts/webscraping/googlesearching');
const uistuff = require('./scripts/ui/uistuff');
const googlesearch = require('./scripts/webscraping/googlesearch');
const findname = require('./scripts/webscraping/findname');
const stance = require('./scripts/webscraping/stance');
const country = require('./scripts/webscraping/country');

async function mainfunc()
{
	chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs =>
	{
		let u = tabs[0].url;
		onGottenPageUrl(u);
	});

}

async function onGottenPageUrl(theUrl)
{
	await new Promise(r => setTimeout(r, 100));

	const subjArticleTitle = await articlestuff.getArticleTitle(theUrl);
	console.log('Article Title Maybe: ' + subjArticleTitle);

	await new Promise(r => setTimeout(r, 100));

	const currArticleName = findname.FindName(theUrl);
	const currArticleStance = stance.PoliticalStance(currArticleName);

	let currArticleStanceImg;
	let currArticleStanceStr;
	if (currArticleStance == 'Left') { currArticleStanceImg = 'images/bias_far_left.png'; currArticleStanceStr = 'Far Left Leaning'; }
	else if (currArticleStance == 'Right') { currArticleStanceImg = 'images/bias_far_right.png'; currArticleStanceStr = 'Far Right Leaning'; }
	else if (currArticleStance == 'Left Centrist') { currArticleStanceImg = 'images/bias_slight_left.png'; currArticleStanceStr = 'Slight Left Leaning'; }
	else if (currArticleStance == 'Right Centrist') { currArticleStanceImg = 'images/bias_slight_right.png'; currArticleStanceStr = 'Slight Right Leaning'; }
	else if (currArticleStance == 'Centrist') { currArticleStanceImg = 'images/bias_centrist.png'; currArticleStanceStr = 'Mainly Centrist'; }

	const currArticle = uistuff.createNewNewsite('(Current Site)');
	uistuff.addBiasLineToNewsite(currArticle, currArticleStanceImg, currArticleStanceStr);

	const farLeft = uistuff.createNewNewsite("Vice News");
	uistuff.addBiasLineToNewsite(farLeft, "images/bias_far_left.png", "Far Left Leaning");

	const slightLeft = uistuff.createNewNewsite("New York Times");
	uistuff.addBiasLineToNewsite(slightLeft, "images/bias_slight_left.png", "Slight Left Leaning");

	const centrist = uistuff.createNewNewsite("BBC");
	uistuff.addBiasLineToNewsite(centrist, "images/bias_centrist.png", "Mainly Centrist");

	const slightRight = uistuff.createNewNewsite("Reason");
	uistuff.addBiasLineToNewsite(slightRight, "images/bias_slight_right.png", "Slight Right Leaning");

	const farRight = uistuff.createNewNewsite("Fox News");
	uistuff.addBiasLineToNewsite(farRight, "images/bias_far_right.png", "Far Right Leaning");

	let sites = [[currArticle, stance.getUrl(currArticleName)], [farLeft, 'vice.com'], [slightLeft, 'nytimes.com'], [centrist, 'bbc.com'], [slightRight, 'reason.com'], [farRight, 'foxnews.com']];

	for (let i = 0; i < sites.length; i++)
	{
		const siteElm = sites[i][0];
		const siteStr = sites[i][1];

		let searchResults = await googlesearch.getGoogleSearchResults(siteStr, subjArticleTitle);
		console.log(searchResults);
		await new Promise(r => setTimeout(r, 500));

		for (let j = 0; j < searchResults.length; j++)
		{
			uistuff.addArticleToNewsite(siteElm, searchResults[j], await articlestuff.getArticleTitle(searchResults[j]), 'Jan 0, 0000');
			await new Promise(r => setTimeout(r, 500));
		}

		await new Promise(r => setTimeout(r, 500));
	}
}

(async () => {
    mainfunc();
})();
```

### styles/styles.css

```css
@import url('https://fonts.googleapis.com/css2?family=Palanquin:wght@400;500;700&display=swap');

:root
{
	--tiny-padding: 1px;
	--small-padding: 5px;
	--medium-padding: 10px;
	--big-padding: 15px;
	--article-inverse-padding: -5px;

	--overall-width: 800px;
	--newsite-width: 200px;
	--corner-rounding: 5px;

	--sans-serif-font: 'Palanquin', sans-serif;

	--blue1: #0b1e33;
	--blue2: #013c58;
	--blue3: #00537a;
	--blue4: #026A9c;
	--yellow1: #f5a201;
	--yellow2: #ffba42;
	--yellow3: #a69577;

	--pol-far-left: #1207A3;
	--pol-slight-left: #8F88D2;
	--pol-centrist: #DCE8ED;
	--pol-slight-right: #DB7A7B;
	--pol-far-right: #BB0103;
}

body
{
	min-width: var(--overall-width);

	margin: 0px;
	padding: 0px;
}

body::-webkit-scrollbar
{
	display: none;
}

.maindiv
{
	background-color: var(--blue1);

	margin: 0px;
	padding: 0px;
}

.main-vert-div
{
	display: flex;
	flex-direction: column;
	align-items: center;

	margin: 0px;
	padding: 0px;
	padding-top: var(--big-padding);

	background-color: var(--blue2);
}

.navbar-div
{
	margin: 0px;
	padding: 0px;
}

.list-of-newsites-div
{
	display: flex;
	flex-wrap: wrap;
	justify-content: center;

	margin: 0px;
	padding: 0px;
	padding-top: var(--big-padding);
}

.list-of-newsites-div > div
{
	margin-right: var(--big-padding);
	margin-bottom: var(--big-padding);
}

.newsite-div
{
	display: flex;
	flex-direction: column;

	min-width: var(--newsite-width);
	max-width: var(--newsite-width);

	margin: 0px;
	padding: var(--medium-padding);

	background-color: var(--blue3);
	border-radius: var(--corner-rounding);
}

.newsite-div > div + div
{
	margin-top: var(--small-padding);
}

.title-and-bias-of-newsite
{
	display: flex;
	flex-direction: column;

	margin: 0px;
	padding: 0px;
}

.bias-of-newsite-line
{
	display: flex;
	flex-wrap: wrap;
	align-items: center;

	margin: 0px;
	padding: 0px;
}

.bias-of-newsite-line > *
{
	margin-right: var(--small-padding);
}

.list-of-articles-dropdown-container
{
	margin: 0px;
	padding: var(--small-padding);

	background-color: var(--blue4);
	border-radius: var(--corner-rounding);
}

.list-of-articles-dropdown
{
	margin: 0px;
	padding: 0px;
}

.list-of-articles-div
{
	display: flex;
	flex-direction: column;

	margin: 0px;
	padding: 0px;
}

.list-of-articles-div > div
{
	margin: 0px;
	padding: 0px;
	padding-top: var(--small-padding);
}

a.article-link
{
	margin: 0px;
	padding: 0px;

	text-decoration: none;
	color: var(--yellow1);
}

.article-div
{
	display: flex;
	flex-direction: column;

	margin: 0px;
	padding: var(--small-padding);
	padding-top: 0px;
	padding-bottom: 0px;

	border-style: solid;
	border-color: var(--blue3);
	border-radius: var(--corner-rounding);
}

h1, h2, h3, h4, h5, h6, p, summary
{
	margin: 0px;
	padding: var(--tiny-padding);
	padding-top: 0px;
	padding-bottom: 0px;

	font-family: var(--sans-serif-font);
	font-weight: 100;
	color: var(--yellow2);
}

h1
{
	font-size: 25px;
	font-weight: 700;
}

h2
{
	font-size: 20px;
	font-weight: 700;
}

.bias-text
{
	font-size: 9px;
	font-weight: 500;
}

.list-of-articles-dropdown > summary
{
	user-select: none;

	font-size: 11px;
	font-weight: 700;
}

h3
{
	font-size: 10px;

	line-height: 10px;
	padding-top: var(--small-padding);
}

.article-type, .article-date
{
	margin-top: var(--article-inverse-padding);

	padding-top: var(--small-padding);

	font-size: 7px;
	color: var(--yellow3);
}

img
{
	margin: 0px;
	padding: var(--tiny-padding);
}
```

### scripts/webscraping/googlesearching.js

```javascript

async function generateGoogleSearchUrl(siteName, articleTitle)
{
	let newTitleA = '' + articleTitle.replaceAll(' ', '+');
	let newTitleB = '';
	for (let i = 0; i < newTitleA.length; i++)
	{
		if (newTitleA.charAt(i).match(/[a-zA-Z0-9\+]/i))
		{ newTitleB += newTitleA.charAt(i); }
	}

	let ret = 'https://www.bing.com/search?q=site:';
	ret += siteName;
	ret += '+' + newTitleB;
	return ret;
}

export { generateGoogleSearchUrl };

```

### scripts/webscraping/nyt.js

```javascript

import axios from 'axios';
const cheerio = require('cheerio');

async function getFirstArticleTitle(url)
{
	url = 'https://cors-anywhere.herokuapp.com/' + url;

	let firstArticleTitle;

	const axiosResponse = await axios.get(url)
	.then(response =>
	{
		const $ = cheerio.load(response.data);
		firstArticleTitle = $('div.app').toString();
	})
	.catch(error =>
	{
		console.error('Error fetching the page:', error);
	});

	return firstArticleTitle;
}

export { getFirstArticleTitle };

```

### scripts/webscraping/articlestuff.js

```javascript
import axios from 'axios';
const cheerio = require('cheerio');

async function getArticleTitle(url)
{
	url = 'https://cors-anywhere.herokuapp.com/' + url;

	// Declare articleTitle outside the block
	let articleTitle;

	// Make a GET request to the URL with User-Agent header
	const axiosResponse = await axios.get(url)
	.then(response => {
		// Load the HTML content into Cheerio
		const $ = cheerio.load(response.data);

		// Find the title from the <title> tag
		articleTitle = $('title').text();

		// Print the article title
		console.log('Article Title:', articleTitle);
	})
	.catch(error => {
		console.error('Error fetching the page:', error);
	});

	return articleTitle;
}

export { getArticleTitle }

```

### scripts/ui/exampleAddingSites.js

```javascript

const uistuff = require('./uistuff')
const articlestuff = require('./../webscraping/articlestuff')


async function thing()
{
	const a = uistuff.createNewNewsite("Site A");
	uistuff.addBiasLineToNewsite(a, "images/bias_far_left.png", "Far Left Leaning");
	uistuff.addArticleToNewsite(a, "https://jrgraphix.net/", "JRGraphix Thing", "Jan 1, 2000");

	const nyt = uistuff.createNewNewsite("New York Times");
	uistuff.addBiasLineToNewsite(nyt, "images/bias_slight_left.png", "Slight Left Leaning");
	const hampshire = 'https://www.nytimes.com/2024/01/20/us/politics/trump-new-hampshire-haley.html';

	uistuff.addArticleToNewsite(nyt, hampshire, await articlestuff.getArticleTitle(hampshire), "Jho 52, 3099");
}

export { thing };

```

### scripts/ui/mainNewsSites.js

```javascript

const uistuff = require('./uistuff')
const articlestuff = require('./../webscraping/articlestuff')

async function addMainNewsSites()
{
	const farLeft = uistuff.createNewNewsite("FarLeftSite");
	uistuff.addBiasLineToNewsite(farLeft, "images/bias_far_left.png", "Far Left Leaning");

	const slightLeft = uistuff.createNewNewsite("SlightLeftSite");
	uistuff.addBiasLineToNewsite(slightLeft, "images/bias_slight_left.png", "Slight Left Leaning");

	const centrist = uistuff.createNewNewsite("CentristSite");
	uistuff.addBiasLineToNewsite(centrist, "images/bias_centrist.png", "Mainly Centrist");

	const slightRight = uistuff.createNewNewsite("SlightRightSite");
	uistuff.addBiasLineToNewsite(slightRight, "images/bias_slight_right.png", "Slight Right Leaning");

	const farRight = uistuff.createNewNewsite("FarRightSite");
	uistuff.addBiasLineToNewsite(farRight, "images/bias_far_right.png", "Far Right Leaning");
}

export { addMainNewsSites };

```

### scripts/webscraping/findname.js

```javascript
function FindName(url) {
    if(url != "Undefined"){
      let address = url;
      let iter = 0;
      let Pcount = 0;
      let first = -1;
      let second = -1;
      let name = "";
    
      if (address.includes("eng.kavkaz-uzel.eu")) {
        return "caucasianknot";
      }
    
      let addressThing = address.indexOf("://");
      addressThing = addressThing + 2;
      //console.log(address.substring(addressThing + 1, addressThing + 4));
      if (address.substring(addressThing + 1, addressThing + 4) == "www") {
        while (Pcount < 2) {
          if (address[iter] == ".") {
            Pcount = Pcount + 1;
            if (first > 0) {
              second = iter;
            } else {
              first = iter;
            }
          }
          iter = iter + 1;
        }
        name = address.substring(first + 1, second);
      } else {
        first = address.indexOf(".");
        name = address.substring(addressThing + 1, first);
      }
    
      console.log(name);
      return name;
      }
    else{
      return "Undefined";
    }
  }
  
  //console.log(FindName("https://reason.com/"));
  
export { FindName };
  
```

[4 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]