diff --git a/.drone.yml b/.drone.yml index 4ac9646..0c6540d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -13,11 +13,9 @@ steps: - name: build image: node commands: - - yarn - yarn build - name: test image: node commands: - - yarn - yarn test diff --git a/README.md b/README.md index 1b5891f..5c83583 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,48 @@ # fediverse-imagebot -A bot that posts images to the Fediverse +[![Build Status](https://ci.rint.osaka/api/badges/NotSam/fediverse-imagebot/status.svg)](https://ci.rint.osaka/NotSam/fediverse-imagebot) + +A bot that posts images to the Fediverse. + +Should be compatible with Mastodon, Misskey and Pleroma! + +## Quick start guide +1. You need to have `npm` and `nodejs` installed. + +2. Install `yarn`: \ +`npm install --global yarn` +- This may be need to ran with `sudo` depending on your installation. + +3. Clone the repository: \ +`git clone https://git.freecumextremist.com/NotSam/fediverse-imagebot.git` + +4. Install dependencies: \ +`yarn` + +5. Build: \ +`yarn build` + +6. Obtain a token. This can be done with an external tool or obtained by running `yarn token` and following the directions. +- `yarn token` will generate a configuration file located at `config.json` + +7. Put images in the `images` folder. +- By default the bot will look for SFW images at `images/sfw` and NSFW images at `images/nsfw`. This can be configured. + +8. Run the bot: \ +`yarn bot` + +You're done! The bot should post an image to the fediverse instance of your choosing! + +## Automating the bot +*TODO: Elaborate more* + +The bot can be automated to post images at set times using a cronjob. \ +Example cron configuration: +``` +0 * * * * cd /path/to/fediverse-imagebot && yarn bot -m "Message" +``` +This example will run the bot every hour on the hour with the post message `Message`. +## Additional information +Additional help can be found by running `yarn bot -h` or `yarn token -h`. + + diff --git a/package.json b/package.json index 9b56ad1..c17ba77 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,16 @@ }, "name": "fediverse-imagebot", "version": "0.1.0", - "description": "Imagebot for the fediverse (Pleroma, Mastodon, Misskey)", + "description": "Image bot for the fediverse (Pleroma, Mastodon, Misskey)", "main": "dist/bot.js", "scripts": { - "build": "npm run clean && tsc", + "build": "yarn run clean && tsc", "clean": "rm -rf dist", + "token": "node ./dist/gen-token.js", "lint": "eslint --ext .ts src", - "test": "echo \"No tests yet!\" && exit 0", - "gen-token": "node ./dist/gen-token.js" + "bot": "node ./dist/bot.js", + "test": "echo \"No tests yet!\" && exit 0" + }, "repository": "https://git.freecumextremist.com/NotSam/fediverse-imagebot.git", "author": "Sam Therapy ", diff --git a/src/bot.ts b/src/bot.ts index 3088e87..03be6fc 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,6 +1,8 @@ +#!/usr/bin/env node import commandLineArgs from "command-line-args"; import commandLineUsage from "command-line-usage"; import * as fs from "fs"; +import generator, { Entity, Response } from "megalodon"; import { exit } from "process"; @@ -19,13 +21,37 @@ const optionDefinitions = [ description: "Print debugging output." }, { - name: "directory", + name: "config", type: String, - alias: "d", - description: "The directory of images for the bot to post.", - defaultValue: "./images", - typeLabel: "" + alias: "c", + description: "Path to the configuration file.", + defaultValue: "./config.json", + typeLabel: "" }, + { + name: "sfw_directory", + type: String, + alias: "s", + description: "The directory of (SFW) images for the bot to post.", + defaultValue: "./images/sfw", + typeLabel: "" + }, + { + name: "nsfw_directory", + type: String, + alias: "n", + description: "The directory of (NSFW) images for the bot to post. If it chooses these, they will be marked sensitive.", + defaultValue: "./images/nsfw", + typeLabel: "" + }, + { + name: "message", + type: String, + alias: "m", + description: "The message to post with the image.", + defaultValue: "", + typeLabel: "" + } ]; const args = commandLineArgs(optionDefinitions); @@ -47,6 +73,53 @@ if (args.help) { console.log(usage); exit(0); } - +// JSON object read from config file const data = JSON.parse(fs.readFileSync("./config.json", "utf8")); -console.log(data); + +const sfw_files = fs.readdirSync(args.sfw_directory); +const nsfw_files = fs.readdirSync(args.nsfw_directory); +const random = Math.floor(Math.random() * (sfw_files.length + nsfw_files.length)); + +// Get image from directory and mark it as sensitive if it's in the nsfw directory +let image: fs.ReadStream; +let sensitive: boolean; +if (random >= sfw_files.length) { + // Random Image is NSFW, mark it sensitive + image = fs.createReadStream(args.nsfw_directory + "/" + nsfw_files[ random - sfw_files.length ]); + sensitive = true; +} +else { + // Image is SFW, mark it not sensitive + image = fs.createReadStream(args.sfw_directory + "/" + sfw_files[ random ]); + sensitive = false; +} + +const client = generator(data.type, data.instance, data.accessToken); +client.uploadMedia(image).then((res: Response) => { + client.postStatus(args.message, { + media_ids: [ res.data.id ], + visibility: "unlisted", + sensitive: sensitive + } + ).then((res: Response) => { + console.log("Successfully posted to " + data.instance); + if (args.verbose) + console.log(console.log(res.data)); + exit(0); + } + ).catch((err: Error) => { + console.error("Error posting to " + data.instance); + console.error("Run with -v to see the full error."); + if (args.verbose) + console.error(err); + exit(1); + } + ); +}).catch((err: Error) => { + console.error("Error uploading image to " + data.instance); + console.error("Run with -v to see the full error."); + if (args.verbose) + console.error(err); + exit(1); +}); + diff --git a/src/gen-token.ts b/src/gen-token.ts index c6c32a7..0937ea4 100644 --- a/src/gen-token.ts +++ b/src/gen-token.ts @@ -21,9 +21,7 @@ const optionDefinitions = [ description: "Print debugging output." } ]; - const args = commandLineArgs(optionDefinitions); - if (args.help) { const usage = commandLineUsage([ { @@ -46,20 +44,23 @@ if (args.verbose) { console.log("Running in verbose mode."); console.log(); } - const instance: string = question("Instance URL: "); callDetector(instance).then(type => { - let clientId!: string; - let clientSecret!: string; const client = generator(type, instance); - client.registerApp("Node Imagebot", { scopes: [ "write" ] }) + client.registerApp("Node Imagebot", { website: "https://git.freecumextremist.com/NotSam/fediverse-imagebot" }) .then((appData) => { - clientId = appData.clientId; - clientSecret = appData.clientSecret; + const clientId = appData.clientId; + const clientSecret = appData.clientSecret; console.log("Please open this URL in your browser for the authorization code."); console.log(appData.url); - const code = question("Authorization Code: "); + let code: string; + if (type === "misskey") { + code = appData.session_token || ""; + question("Authenticate with Misskey, then hit return."); + } else { + code = question("Authorization Code: "); + } client.fetchAccessToken(clientId, clientSecret, code) .then((tokenData: OAuth.TokenData) => { if (args.verbose) { @@ -82,7 +83,6 @@ callDetector(instance).then(type => { console.error(err); exit(1); }); - }) .catch((err: Error) => { // catch for registerApp console.error("App registration failure!"); @@ -92,7 +92,6 @@ callDetector(instance).then(type => { }); }); - async function callDetector(instance: string) { const type = await detector(instance).catch( (err) => { console.error("This does not seem to be a valid instance!");