Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
mindlapse committed Dec 27, 2022
0 parents commit 0e347ce
Show file tree
Hide file tree
Showing 13 changed files with 4,344 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build
node_modules
*.js
.env.*
!.env.sample
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Mastodon - Spicy Tags

Monitor a Mastodon instance for interesting tags.

To get started, create an API key within Mastodon for your account.


Next, create a `./env/.env.<suffix>` file using the `.env.sample` file as a template.

Set the following variables to match your setup:
```
API_URL=https://mastodon.social/api/v1/
ACCOUNT_ID=112233445566778899
SLEEP_MS=120000
ACCESS_TOKEN=abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG
```


Start polling with:

```
npm start --env=<suffix>
```
20 changes: 20 additions & 0 deletions env/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@


# The base URL for the API - for example: https://mastodon.social/api/v1/
#
API_URL=https://mastodon.social/api/v1/


# The Mastodon account ID
#
ACCOUNT_ID=112233445566778899


# Time to sleep between fetches
#
SLEEP_MS=120000


# An access token for the above Mastodon account
#
ACCESS_TOKEN=abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG
65 changes: 65 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import sleep from "sleep-promise";
import Followers from "./lib/followers";
import Tags from "./lib/tags";
import spicyTags from "./spicyTags.json";
import { API_URL, ACCESS_TOKEN, ACCOUNT_ID, SLEEP_MS } from "./lib/env";

const Mastodon = require("mastodon-api");

const M = new Mastodon({ api_url: API_URL, access_token: ACCESS_TOKEN });
const tags = new Tags(spicyTags.spicyTags);

const main = async () => {

/*
Watch the federated stream for key hashtags. For each post:
IF
- a post contains one of the 'spicy' hashtags
- and the account is not already followed (or previously logged)
THEN
- log the account to console, and show the spicy tags from its post.
ELSE
- Show the current tags
*/

const allTagsSeen = new Set();
const following = new Followers(M, ACCOUNT_ID);
const follows = await following.load();
console.log(`Loaded ${follows.size} following`);

do {
const result = await M.get("timelines/public", {});
const newTags = new Set();
const batchTags = new Set();
for (let item of result.data) {
const user = item.account.acct;

// Skip the user if it is in the follows Set
if (follows.has(user)) {
continue;
}

// Collect tags from the batch
item.tags.forEach((t: any) => {
if (!allTagsSeen.has(t.name)) {
allTagsSeen.add(t.name);
newTags.add(t.name);
}
batchTags.add(t.name);
});

// If the batch has a post with a spicy tag, log the user & spicy tags
const spicy = tags.getSpicyTags(item.tags);
if (spicy && spicy.length !== 0) {
console.log(`\n${user} #${spicy}`);
follows.add(user);
}
}

// Show timestamped tags for this batch/iteration
console.log(`${new Date().toISOString()} #${Array.from(newTags)}`);
await sleep(SLEEP_MS);
} while (true);
};

main().catch((e) => console.error(e));
27 changes: 27 additions & 0 deletions lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Load the .env.<suffix> file, where the suffix is given by `npm start --env <suffix>`
if (!process.env.npm_config_env) {
console.log("Missing --env parameter to select the dotenv config by suffix");
console.log(
"As an example, you can pass `npm start --env=social` to load ./env/.env.social) "
);
process.exit(1);
}
const path = "env/.env." + process.env.npm_config_env;
require("dotenv").config({ path });

if (!process.env.API_URL) {
console.log(`Missing ${process.env.npm_config_env}`);
process.exit(2);
}

// The base URL for the API - for example: https://mastodon.social/api/v1/
export const API_URL = process.env.API_URL!;

// The Mastodon account ID
export const ACCOUNT_ID = process.env.ACCOUNT_ID!;

// An access token for the above Mastodon account
export const ACCESS_TOKEN = process.env.ACCESS_TOKEN;

// Time to sleep between fetches
export const SLEEP_MS = parseInt(process.env.SLEEP_MS!);
52 changes: 52 additions & 0 deletions lib/followers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { IncomingMessage } from "http";

export default class Following {
private M: any;
private accountId: string;

constructor(mastodon: any, accountId: string) {
this.M = mastodon;
this.accountId = accountId;
}

/*
Return the user addresses that the account is following
*/
load = async () => {
let allFollowers: string[] = [];
let maxId = undefined;
let result;
do {
result = await this.loadFollowingBatch(maxId);
console.log(`Loading followers ${maxId}`);
maxId = result.nextId;

allFollowers = allFollowers.concat(result.batch);
} while (maxId);

return new Set(allFollowers);
};

private loadFollowingBatch = async (maxId?: number) => {
if (typeof maxId === "undefined") {
maxId = 1000000000;
}
const response = await this.M.get(
`accounts/${this.accountId}/following?max_id=${maxId}`
);
const nextId = this.getNextMaxId(response.resp);

const batch = response.data.map((f: any) => f.acct);

return {
batch,
nextId,
};
};

private getNextMaxId = (resp: IncomingMessage) => {
const link = resp.headers["link"] as string;
const matches = link?.match(/max_id=([0-9]+)/);
return matches ? parseInt(matches[1]) : undefined;
};
}
26 changes: 26 additions & 0 deletions lib/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default class Tags {
private spicyTags: Set<string>;

constructor(spicyTags: string[]) {
this.spicyTags = new Set(spicyTags);
}

/*
Get the intersection of the given tags with the spicyTags as an array
*/
getSpicyTags(tags: NameUrl[]) {
const found = [];
for (let tag of tags) {
let tagName = tag.name.toLowerCase();
if (this.spicyTags.has(tagName)) {
found.push(tagName);
}
}
return found;
}
}

interface NameUrl {
name: string;
url: string;
}
Loading

0 comments on commit 0e347ce

Please sign in to comment.