Compare commits

..

No commits in common. "master" and "1.1.0" have entirely different histories.

28 changed files with 2236 additions and 2750 deletions

View file

@ -1,91 +0,0 @@
local pipe(arch) = {
kind: 'pipeline',
type: 'docker',
name: arch,
platform: {
arch: arch,
},
steps: [
{
name: 'deps',
image: 'node',
commands: [
'corepack enable && pnpm config set store-dir .cache/pnpm',
'pnpm i',
],
},
{
name: 'lint',
image: 'node',
commands: [
'npm run lint:ci',
],
depends_on: [
'deps',
],
},
{
name: 'build',
image: 'node',
commands: [
'npm run build',
],
depends_on: [
'lint',
],
},
],
trigger: {
event: {
exclude: [
'tag',
],
},
},
};
local release(arch) = {
kind: 'pipeline',
type: 'docker',
name: 'release-%s' % [arch],
platform: {
arch: arch,
},
trigger: {
event: [
'tag',
],
},
steps: [
{
name: 'build',
image: 'node:lts',
commands: [
'corepack enable && pnpm config set store-dir .cache/pnpm',
'pnpm i',
'pnpm build',
],
},
{
name: 'publish',
image: 'plugins/npm',
settings: {
token: {
from_secret: 'release_api_key',
},
registry: 'https://git.froth.zone/api/packages/sam/npm/',
},
depends_on: [
'build',
],
},
],
};
[
pipe('amd64'),
pipe('arm64'),
release('amd64'),
// release('arm64'),
]

56
.drone.yml Normal file
View file

@ -0,0 +1,56 @@
kind: pipeline
type: docker
name: default
steps:
- name: dependencies
image: node
commands:
- yarn
- name: lint
image: node
depends_on:
- dependencies
commands:
- yarn lint
- name: build
image: node
depends_on:
- dependencies
commands:
- yarn build
- name: test
image: node
depends_on:
- build
commands:
- yarn test
- name: package
image: node
depends_on:
- lint
- test
commands:
- yarn package
when:
event:
- tag
- name: Release
image: plugins/gitea-release
depends_on:
- package
settings:
api_key:
from_secret: release_api_key
base_url: https://git.froth.zone
files:
- dist/imagebot-*
when:
event:
- tag

View file

@ -1 +0,0 @@
dist/

View file

@ -1,24 +1,36 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "never"],
"prettier/prettier": ["error", { "singleQuote": false, "semi": false }]
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
]
"env": {
"commonjs": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 13
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
}

14
.gitignore vendored
View file

@ -112,15 +112,11 @@ dist
.vscode-test
# yarn v2
.yarn/*
!.yarn/cache
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.dccache
config.json
config.jsonc
config.json5
images/*

View file

@ -1,2 +0,0 @@
dist/
.yarn/

17
LICENSE
View file

@ -1,10 +1,9 @@
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
MIT License
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
Copyright (c) 2022 Sam Therapy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,87 +1,50 @@
# fediverse-imagebot
[![Build Status](https://ci.git.froth.zone/api/badges/sam/fediverse-imagebot/status.svg)](https://ci.git.froth.zone/sam/fediverse-imagebot)
[![Build Status](https://ci.git.froth.zone/api/badges/Sam/fediverse-imagebot/status.svg)](https://ci.git.froth.zone/Sam/fediverse-imagebot)
A bot that posts local and booru images to the Fediverse.
A bot that posts (currently only local) images to the Fediverse.
Compatible with Mastodon, Misskey and Pleroma!
## MIGRATING MAJOR VERSIONS
See [the wiki](https://git.froth.zone/sam/fediverse-imagebot/wiki/Migrating).
## Boorus supported
The full list of boorus supported is found [here](https://github.com/AtoraSuunva/booru/blob/master/src/sites.json).
## Downloading the bot
There are currently three ways to do this, a pre-built binary that bundles in node, from npm, or building from source. Both are listed below.
### From NPM
1. Set up using the [Gitea registry](https://git.froth.zone/sam/fediverse-imagebot/packages)
```sh
npm config set @froth:registry https://git.froth.zone/api/packages/sam/npm/
```
2. After setting up the registry, either run it once
```sh
npx --package=@froth/fediverse-imagebot fediverse-imagebot
```
or install globally
```sh
npm i -g @froth/fediverse-imagebot
```
### Downloading pre-built binaries
#### NOTE: This is no longer supported since vercel/pkg is broken
Download prebuilt binaries from [here](https://git.froth.zone/sam/fediverse-imagebot/releases/latest). (currently supports x86_64 and arm64 on Linux
[glibc or musl], macOS and Windows)
1. Run the bot with the `-w` flag to have it generate a configuration file to the local directory.
2. Edit it for your use case.
- If you want to generate a key you can use <https://git.froth.zone/sam/js-feditoken>
3. Run the bot by launching the executable!
### Running from Source
## Downloading pre-built binaries
Download prebuilt binaries from [here](https://git.froth.zone/Sam/fediverse-imagebot/releases/latest) (currently only supports x64), or build from source, instructions below.
## Running from Source
1. You need to have `npm` and `nodejs` installed.
- Node 16 or greater is required.
2. Install `pnpm`: \
`corepack enable` \
Check <https://pnpm.io/installation> for more information.
- Node 15 or greater is required.
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.froth.zone/sam/fediverse-imagebot.git`
`git clone https://git.freecumextremist.com/NotSam/fediverse-imagebot.git`
4. Install dependencies: \
`pnpm i`
`yarn --production`
5. Build: \
`pnpm run build`
6. Edit the config file: \
`cp config.sample.jsonc config.jsonc`
- If you want to generate a key you can use https://git.froth.zone/sam/js-feditoken
7. Run the bot: \
`pnpm bot`
`yarn build`
8. Run the bot: \
`yarn local`
You're done! The bot should post a local image to the fediverse instance of your choosing!
## Automating the bot
## Running the bot
1. Obtain a token. I have another tool that does this for you, which can be found [here](https://git.froth.zone/Sam/js-feditoken) and put it in `config.json`, following the sample json file.
_TODO: Elaborate more_
2. 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 with the `-d` flag.
## 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 && pnpm bot -c ./config.sample.jsonc
0 * * * * cd /path/to/fediverse-imagebot && /usr/local/bin/yarn local -m "Message"
```
This example will run the bot every hour on the hour with no message using images from the default `images` directory.
This example will run the bot every hour on the hour with the post message `Message` using images from the default `images` directory.
An example of this pleroma configuration can be found at https://froth.zone/rinbot.

View file

@ -1,33 +1,8 @@
// This is just a mock of what the config should look like
// Run yarn build && yarn gen-token to generate the configuration.
{
//Instance and token settings
"instance": "INSTANCE_URL", // example https://test.com
"type": "INSTANCE_TYPE", // examples: "mastodon", "misskey", "pleroma"
"accessToken": "ACCESS_TOKEN", // Get a token from https://git.froth.zone/Sam/js-feditoken
"refreshToken": "REFRESH_TOKEN", // optional
// Post settings
"message": "", // example: "Hello, world!"
"visibility": "unlisted", // example: "public", "unlisted", "private", "direct"
// Misc settings
"retries": 5, // Number of times to retry an upload/post if it fails. Default is 5 if this does not exist.
"remote": false, // **Set this to `true` if you want to serve a file from a booru!**
/* THESE SETTINGS WILL BE IGNORED IF YOU SET `remote` TO `false` */
"booru": "safebooru.org", // example: "safebooru.org"
"tags": [""], // example: ["tohsaka_rin", "-feet"]
"rating": "safe", // example: "safe", "questionable", "explicit"
/* END OF SETTINGS THAT WILL BE IGNORED IF YOU SET `remote` TO `false` */
/* THESE SETTINGS WILL BE IGNORED IF YOU SET `remote` TO `true` */
"directory": "./images" // example: "./images"
/*
Directory structure should be as follows:
folder/
- sfw/
- image1.jpg
- nsfw/
- image1.jpg
*/
/* END OF SETTINGS THAT WILL BE IGNORED IF YOU SET `remote` TO `true` */
}
"accessToken": "ACCESS_TOKEN",
"refreshToken": "REFRESH_TOKEN" // This can be left blank, as it is currently not used for anything.
}

View file

@ -1,53 +1,37 @@
{
"dependencies": {
"booru": "2.6.3",
"command-line-args": "5.2.1",
"command-line-usage": "7.0.1",
"got": "12.6.1",
"json5": "2.2.3",
"megalodon": "6.0.2",
"p-retry": "5.1.2"
},
"name": "@froth/fediverse-imagebot",
"version": "2.1.1",
"description": "Image bot for the fediverse (Pleroma, Mastodon, Misskey)",
"main": "dist/bot.js",
"bin": "dist/bot.js",
"pkg": {
"scripts": "dist/**/*.js"
},
"scripts": {
"build": "tsc -b",
"clean": "tsc -b --clean",
"lint": "eslint --ext .ts ./src --fix && prettier --no-semi --write ./src",
"lint:ci": "eslint --ext .ts,.js ./src && prettier --no-semi --check ./src",
"bot": "node ./dist/bot.js",
"package": "pkg . -C Gzip",
"test": "echo \"No tests yet!\" && exit 0",
"prepublishOnly": "npm run build"
},
"repository": "https://git.froth.zone/sam/fediverse-imagebot",
"author": "Sam Therapy <sam@samtherapy.net>",
"license": "0BSD",
"devDependencies": {
"@types/command-line-args": "5.2.0",
"@types/command-line-usage": "5.0.2",
"@types/http-cache-semantics": "4.0.1",
"@types/node": "18.16.16",
"@typescript-eslint/eslint-plugin": "5.59.7",
"@typescript-eslint/parser": "5.59.7",
"eslint": "8.41.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-prettier": "4.2.1",
"pkg": "5.8.1",
"prettier": "2.8.8",
"typescript": "5.0.4"
},
"files": [
"dist"
],
"type": "module",
"publishConfig": {
"registry": "https://git.froth.zone/api/packages/sam/npm/"
"dependencies": {
"command-line-args": "^5.2.1",
"command-line-usage": "^6.1.1",
"megalodon": "^4.0.0",
"typescript": "^4.5.5"
},
"name": "fediverse-imagebot",
"version": "1.1.0",
"description": "Image bot for the fediverse (Pleroma, Mastodon, Misskey)",
"main": "dist/local.js",
"bin": "dist/local.js",
"pkg": {
"scripts": "build/**/*.js",
"targets": ["node16-linux-x64","node16-macos-x64","node16-win-x64"]
},
"scripts": {
"build": "yarn run clean && tsc",
"clean": "rm -rf dist",
"lint": "eslint --ext .ts src",
"local": "node ./dist/local.js",
"package": "pkg . -o dist/imagebot-x64 -C Gzip",
"test": "echo \"No tests yet!\" && exit 0"
},
"repository": "https://git.froth.zone/Sam/fediverse-imagebot",
"author": "Sam Therapy <sam@samtherapy.net>",
"license": "MIT",
"devDependencies": {
"@types/command-line-args": "^5.2.0",
"@types/command-line-usage": "^5.0.2",
"@types/node": "^17.0.18",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"eslint": "^8.9.0",
"pkg": "^5.5.2"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"rangeStrategy": "pin"
}

View file

@ -1,23 +0,0 @@
#!/usr/bin/env node
import getLocalImage from "./getLocalImage.js"
import getRemoteImage from "./getRemoteImage.js"
import getConfig from "./helpers/getConfig.js"
import { config } from "./helpers/types.js"
/**
* Main function
*/
async function main() {
const conf: config = await getConfig()
if (conf.remote) await getRemoteImage(conf)
else await getLocalImage(conf)
}
if (Number(process.versions.node.split(".")[0]) < 16) {
console.error("Please upgrade to node 16. NO GUARENTEES!")
}
// Run the main function, obviously.
main()

View file

@ -1,62 +0,0 @@
import { createReadStream, ReadStream } from "node:fs"
import { readdir } from "node:fs/promises"
import { exit } from "node:process"
import postImage from "./postImage.js"
import args from "./helpers/args.js"
import crashHandler from "./helpers/crashHandler.js"
import { config } from "./helpers/types.js"
/**
* Get a local image from the filesystem
* @param conf Configuration object (see {@link config})
* @calls postImage with the image it randomly selects from the local directory
*/
export default async function getLocalImage(conf: config) {
// Get SFW directory
const sfw_files: string[] = await readdir(`${conf.directory}/sfw`).catch(
(e) => {
crashHandler("Error reading SFW image directory.", e)
return []
}
)
// Get NSFW directory
const nsfw_files: string[] = await readdir(`${conf.directory}/nsfw`).catch(
(e) => {
crashHandler("Error reading NSFW image directory.", e)
return []
}
)
const random = Math.floor(
Math.random() * (sfw_files.length + nsfw_files.length)
)
// Filler that is used to get a random file from the directories
let image: ReadStream
let sensitivity: boolean
let file: string
if (random >= sfw_files.length) {
// Image is NSFW, mark it sensitive
file = `${conf.directory}/nsfw/${nsfw_files[random - sfw_files.length]}`
image = createReadStream(file).on("error", (err: Error) => {
crashHandler(`Error reading file "${file}"`, err)
})
sensitivity = true
} else {
// Image is SFW, mark it not sensitive
file = `${conf.directory}/sfw/${sfw_files[random]}`
image = createReadStream(file).on("error", (err: Error) => {
crashHandler(`Error reading file "${file}"`, err)
})
sensitivity = false
}
if (args.verbose) {
console.log(`File being sent: ${file}`)
console.log(`Sensitivity: ${sensitivity}`)
}
await postImage(image, sensitivity, conf)
exit(0)
}

View file

@ -1,66 +0,0 @@
import { createReadStream, createWriteStream } from "node:fs"
import { unlink } from "node:fs/promises"
import { exit } from "node:process"
import stream from "node:stream"
import { promisify } from "node:util"
import { search, Post } from "booru"
import got from "got"
import postImage from "./postImage.js"
import args from "./helpers/args.js"
import crashHandler from "./helpers/crashHandler.js"
import { config } from "./helpers/types.js"
/**
* Get a remote image from a booru
*
* Currently this also downloads the image locally but idc nmp it gets deleted
* @param conf Configuration object (see {@link config})
* @calls postImage with the image it randomly selects from the booru given
*/
export default async function getRemoteImage(conf: config) {
const searchResults = await search(conf.booru, conf.tags, {
limit: 1,
random: true,
}).catch((e) => {
crashHandler("Error searching for posts.", e)
return [] as Post[]
})
if (searchResults.length === 0) {
crashHandler("Error searching for posts.", Error("No posts found."))
return
}
const post = searchResults[0]
if (args.verbose) console.log(`Found post: ${post.id} at ${post.file_url}`)
// Set the post as sensitive if the rating is not safe
const sensitivity: boolean = post.rating !== "s"
// Make an HTTP request for the image
const filename: string = post.fileUrl?.split("/").pop() as string // Type checks for type checks
const pipeline = promisify(stream.pipeline)
// Make the HTTP request as a stream so it can be piped to the file system
await pipeline(
got.stream(post.file_url as string),
createWriteStream(filename)
).catch((err: Error) => {
crashHandler("Error saving downloading image.", err)
})
if (args.verbose) console.log(`Saved image to ${filename}`)
const str = createReadStream(filename).on("error", (err: Error) => {
crashHandler("Error reading downloaded image.", err)
})
if (args.verbose) {
console.log(`File being sent: ${filename}\nSensitivity: ${sensitivity}`)
}
// Make a status with the image
await postImage(str, sensitivity, conf)
// Delete the image that it downloaded
await unlink(filename).catch((err: Error) => {
crashHandler("Error deleting downloaded image.", err)
})
if (args.verbose) console.log(`Successfully deleted image ${filename}`)
exit(0)
}

View file

@ -1,78 +0,0 @@
import { exit } from "node:process"
import commandLineArgs from "command-line-args"
import commandLineUsage from "command-line-usage"
import writeConfig from "./writeConfig.js"
const optionDefinitions = [
{
name: "help",
type: Boolean,
alias: "h",
description: "Print this usage guide.",
},
{
name: "verbose",
type: Boolean,
alias: "v",
defaultValue: false,
description: "Print debugging output.",
},
{
name: "config",
type: String,
alias: "c",
description:
"Path to the JSON configuration file. (default: ./config.jsonc)",
defaultValue: "./config.jsonc",
typeLabel: "<file.json[c,5]>",
},
{
name: "writeConfig",
type: Boolean,
alias: "w",
description:
"Write a default configuration file to the current directory and exit.",
},
{
name: "message",
type: String,
alias: "m",
description: "The message to post with the image.",
defaultValue: "",
typeLabel: "<message>",
},
]
const args = commandLineArgs(optionDefinitions)
if (args.help) {
const usage = commandLineUsage([
{
header: "Fediverse Image Bot",
content:
"A bot that posts images from a local directory to the Fediverse.",
},
{
header: "Options",
optionList: optionDefinitions,
},
{
content:
"Project home: {underline https://git.froth.zone/Sam/fediverse-imagebot}",
},
])
console.log(usage)
exit(0)
}
if (args.verbose) console.log("Running in verbose mode.\n")
if (args.writeConfig) {
writeConfig(args.verbose).then(() => {
console.log("Wrote default config file to ./config.jsonc")
exit(0)
})
}
export default args

69
src/helpers/cli.ts Normal file
View file

@ -0,0 +1,69 @@
import commandLineArgs from "command-line-args";
import commandLineUsage from "command-line-usage";
import { exit } from "process";
const optionDefinitions = [
{
name: "help",
type: Boolean,
alias: "h",
description: "Print this usage guide."
},
{
name: "verbose",
type: Boolean,
alias: "v",
description: "Print debugging output."
},
{
name: "config",
type: String,
alias: "c",
description: "Path to the JSON configuration file. (default: ./config.json)",
defaultValue: "./config.json",
typeLabel: "<file.json>"
},
{
name: "directory",
type: String,
alias: "d",
description: "The directory of images to upload. (default: ./images)",
defaultValue: "./images",
typeLabel: "<folder>"
},
{
name: "message",
type: String,
alias: "m",
description: "The message to post with the image.",
defaultValue: "",
typeLabel: "<message>"
}
];
const args = commandLineArgs(optionDefinitions);
if (args.help) {
const usage = commandLineUsage([
{
header: "Fediverse Image Bot",
content: "A bot that posts images from a local directory to the Fediverse."
},
{
header: "Options",
optionList: optionDefinitions
},
{
content: "Project home: {underline https://git.froth.zone/Sam/fediverse-imagebot}"
}
]);
console.log(usage);
exit(0);
}
if (args.verbose) {
console.log("Running in verbose mode.\n");
}
export default args;

20
src/helpers/config.ts Normal file
View file

@ -0,0 +1,20 @@
import args from "./cli";
import crashHandler from "./errors";
import { readFile } from "fs/promises";
// Read the config file and return the contents as a JSON object
export default async function config() {
try {
return JSON.parse(
await readFile(args.config, "utf8")
.catch(err => {
crashHandler("Error reading config file.", err);
return ("");
})
);
}
catch (e: unknown) {
crashHandler("Error reading config file.", e);
}
}

View file

@ -1,18 +0,0 @@
import { exit } from "node:process"
import args from "./args.js"
/**
* The function that gets called when the program runs into an error.
* @param msg Custom error message
* @param e Error object passed in from the error handler
* @returns This function will never return.
*/
export default function crashHandler(msg: string, e: Error, res?: string) {
console.error(`${msg}: ${e.name}`)
if (args.verbose) {
console.error(`--BEGIN FULL ERROR--\n${e}\n${res}\n--END FULL ERROR--`)
} else console.error("Run with -v to see the full error.")
exit(1)
}

13
src/helpers/errors.ts Normal file
View file

@ -0,0 +1,13 @@
import { exit } from "process";
import args from "./cli";
// Boilerplate for the event any error occurs
export default function crashHandler(msg: string, e: Error | unknown) {
console.error(msg);
if (args.verbose) {
console.error(`--BEGIN FULL ERROR--\n${e}\n--END FULL ERROR--`);
} else
console.error("Run with -v to see the full error.");
exit(1);
}

View file

@ -1,49 +0,0 @@
import { readFile } from "node:fs/promises"
import JSON5 from "json5"
import args from "./args.js"
import crashHandler from "./crashHandler.js"
import { config } from "./types.js"
/**
* Reads the config file and returns the config object
* @returns The config object
*/
async function readConfig(): Promise<string> {
return readFile(args.config, "utf8").catch((err) => {
crashHandler("Error reading config file.", err)
return "CRASH"
})
}
/**
* Parses the config file and returns it as a JSON object
*
* See config.sample.jsonc for an example of the config file
* @returns {Promise<config>} The config file as a JSON object(see {@link config})
* @example
* const cfg = await getConfig();
* console.log(cfg.instance);
* // Prints "https://mastodon.social"
*/
export default async function getConfig(): Promise<config> {
let conf: config
try {
conf = JSON5.parse(
await readConfig().catch((err) => {
crashHandler("Error reading config file.", err)
return ""
})
)
// Backwards compatibility for older versions
conf.retries ??= 5
if (conf.retries < 1) conf.retries = 1
} catch (err: unknown) {
crashHandler("Error parsing config file.", Error(err as string))
return {} as config
}
if (args.verbose) {
console.log(`Read config file: ${args.config}\n${JSON.stringify(conf)}`)
}
return conf
}

View file

@ -1,16 +0,0 @@
/**
* Config type to reduce boilerplate
*/
export type config = {
instance: string
type: "misskey" | "mastodon" | "pleroma"
accessToken: string
refreshToken: string | null
message: string
visibility: "direct" | "unlisted" | "private" | "public"
remote: boolean
booru: string
tags: string[]
directory: string
retries: number
}

View file

@ -1,14 +0,0 @@
import { writeFile } from "node:fs/promises"
/**
* Writes the sample config file to disk
* @returns Nothing
*/
export default async function writeConfig(verbose: boolean) {
if (verbose) console.log("Writing sample config to config.jsonc")
await writeFile(
"./config.jsonc",
// eslint-disable-next-line prettier/prettier
// prettier-ignore
"{ \n //Instance and token settings \n \"instance\": \"INSTANCE_URL\", // example https://test.com \n \"type\": \"INSTANCE_TYPE\", // examples: \"mastodon\", \"misskey\", \"pleroma\" \n \"accessToken\": \"ACCESS_TOKEN\", // Get a token from https://git.froth.zone/Sam/js-feditoken \n \"refreshToken\": \"REFRESH_TOKEN\", // optional \n \n // Post settings \n \"message\": \"\", // example: \"Hello, world!\" \n \"visibility\": \"unlisted\", // example: \"public\", \"unlisted\", \"private\", \"direct\" \n \n \"remote\": false, // **Set this to `true` if you want to serve a file from a booru!** \n /* THESE SETTINGS WILL BE IGNORED IF YOU SET `remote` TO `false` */ \n \"booru\": \"safebooru.org\", // example: \"safebooru.org\" \n \"tags\": [\"\"], // example: [\"tohsaka_rin\", \"-feet\"] \n \"rating\": \"safe\", // example: \"safe\", \"questionable\", \"explicit\" \n /* END OF SETTINGS THAT WILL BE IGNORED IF YOU SET `remote` TO `false` */ \n \n /* THESE SETTINGS WILL BE IGNORED IF YOU SET `remote` TO `true` */ \n \"directory\": \"./images\" // example: \"./images\" \n /* \n Directory structure should be as follows: \n folder/ \n - sfw/ \n - image1.jpg \n - nsfw/ \n - image1.jpg \n */ \n /* END OF SETTINGS THAT WILL BE IGNORED IF YOU SET `remote` TO `true` */ \n} \n"
)
}

53
src/local.ts Normal file
View file

@ -0,0 +1,53 @@
import { createReadStream, ReadStream } from "fs";
import { readdir } from "fs/promises";
import { exit } from "process";
import args from "./helpers/cli";
import crashHandler from "./helpers/errors";
import post from "./post";
export default async function getLocalImage() {
const sfw_files: string[] = await readdir(`${args.directory}/sfw`).catch(e => {
crashHandler("Error reading SFW image directory.", e);
return [];
});
const nsfw_files: string[] = await readdir(`${args.directory}/nsfw`).catch(e => {
crashHandler("Error reading NSFW image directory.", e);
return [];
});
const random = Math.floor(Math.random() * (sfw_files.length + nsfw_files.length));
// Filler that is used to get a random file from the directories
let image: ReadStream;
let sensitivity: boolean;
let file = "";
if (random >= sfw_files.length) {
// Image is NSFW, mark it sensitive
file = `${args.directory}/nsfw/${nsfw_files[ random - sfw_files.length ]}`;
image = createReadStream(file)
.on("error", (err: Error) => {
crashHandler(`Error reading file "${file}"`, err);
});
sensitivity = true;
}
else {
// Image is SFW, mark it not sensitive
file = `${args.directory}/sfw/${sfw_files[ random]}`;
image = createReadStream(file)
.on("error", (err: Error) => {
crashHandler(`Error reading file "${file}"`, err);
});
sensitivity = false;
}
if (args.verbose) {
console.error(`File being sent: ${file}`);
console.error(`Sensitivity: ${sensitivity}`);
}
await post(image, sensitivity);
exit(0);
}
getLocalImage();

43
src/post.ts Normal file
View file

@ -0,0 +1,43 @@
import { ReadStream } from "fs";
import generator, { Entity, Response } from "megalodon";
import config from "./helpers/config";
import crashHandler from "./helpers/errors";
import args from "./helpers/cli";
// Uploads an image to the Fediverse
// image: The image to upload
// sensitivity: Whether or not the image is sensitive
export default async function post(image: ReadStream, sensitivity: boolean) {
// Get config
const cfg: {
instance: string,
type: "misskey" | "mastodon" | "pleroma",
accessToken: string,
refreshToken: string | null
} = await config();
// Make a client to upload
const client = generator(cfg.type, cfg.instance, cfg.accessToken, cfg.refreshToken);
// Upload the image
const res: Response<Entity.Attachment> = await client.uploadMedia(image).catch(err => {
crashHandler("Error uploading image.", err);
return ({} as Response<Entity.Attachment>);
});
// Make a status with the image
await client.postStatus(args.message,
{
media_ids: [ res.data.id ],
// Change this to make the post visibility you wish
visibility: "unlisted",
sensitive: sensitivity
}
).catch(err => {
crashHandler("Error posting status.", err);
});
if (args.verbose)
console.log(`Successfully posted to ${cfg.instance}`);
return;
}

View file

@ -1,64 +0,0 @@
import { ReadStream } from "node:fs"
import { Readable } from "node:stream"
import generatorPkg, { Entity, Response } from "megalodon"
const generator = generatorPkg.default
import pRetry from "p-retry"
import args from "./helpers/args.js"
import crashHandler from "./helpers/crashHandler.js"
import { config } from "./helpers/types.js"
/**
* Uploads an image to a fediverse instance
* @param image The image to upload
* @param sensitivity The sensitivity of the image
* @param cfg {@link config} object
* @returns { Promise<void> } Nothing
*/
export default async function postImage(
image: ReadStream | Readable,
sensitivity: boolean,
cfg: config
): Promise<void> {
// Make a client to upload
const client = generator(
cfg.type,
cfg.instance,
cfg.accessToken,
cfg.refreshToken
)
// Upload the image
const upload = async () => client.uploadMedia(image)
const res = await pRetry(upload, {
retries: cfg.retries,
onFailedAttempt: logRetry,
}).catch((err) => {
crashHandler("Error uploading image.", err, err.response.data)
return {} as Response<Entity.Attachment>
})
// Make the post
const post = async () =>
client.postStatus(args.message || cfg.message, {
media_ids: [res.data.id],
// Change this to make the post visibility you wish
visibility: cfg.visibility,
sensitive: sensitivity,
})
await pRetry(post, {
retries: cfg.retries,
onFailedAttempt: logRetry,
}).catch((err) => {
crashHandler("Error uploading image", err, err.response.data)
})
console.log(`Successfully posted to ${cfg.instance}`)
}
function logRetry(error: unknown) {
console.error("Retrying, error:")
console.error(error)
}

View file

@ -1,37 +1,61 @@
{
"compilerOptions": {
/* Basic Options */
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": [
"es6"
] /* Specify library files to be included in the compilation. */,
"outDir": "./dist" /* Redirect output structure to the directory. */,
"removeComments": true /* Do not emit comments to output. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"importHelpers": false /* Import emit helpers from 'tslib'. */,
"downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */,
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es6"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /*es Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
"removeComments": true, /* Do not emit comments to output. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
// "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "NodeNext" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"sourceMap": true /* Generates corresponding '.map' file. */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": ["./src"]
}

1819
yarn.lock Normal file

File diff suppressed because it is too large Load diff