WIP
parent
73315a2d77
commit
f07baecee4
18
.env.sample
18
.env.sample
|
@ -1,14 +1,4 @@
|
|||
# Minimal options
|
||||
INSTANCE=mastodon.social
|
||||
TOKEN=xxxxxxxxxxxxxxxxxx
|
||||
DATA_STATUS=yyyyyyyyyyyy
|
||||
|
||||
# Basic options
|
||||
VISIBILITY=unlisted
|
||||
TARGET=global
|
||||
# TARGET_STATUSES=id1,id2,id3
|
||||
TIMEOUT=60000
|
||||
|
||||
# Moderation
|
||||
MODERATION=self
|
||||
# MODERATION_LIMIT=5
|
||||
DOMAIN=mastodon.social
|
||||
TOKEN=xxxxxxxxxxxxxxxx
|
||||
ROOT_STATUS=yyyyyyyyyy
|
||||
TIMEOUT=60000
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
*dist
|
||||
*node_modules
|
||||
.env
|
||||
.env
|
||||
.package-lock
|
121
README.md
121
README.md
|
@ -11,8 +11,10 @@ A pretty simple random message bot using [Mastodon](https://joinmastodon.org) st
|
|||
|
||||
- Create an account for this bot. Please mark it as bot in configuration page.
|
||||
- In settings page, create a new application with read write permission and copy the private key.
|
||||
- Create a status to store messages (could be private) and copy his id. *Called `data status` after*
|
||||
- Reply to this status to add content. (Removing @acct, to limit self spam)
|
||||
- Create a status to store actions (could be private) and copy its id. *Called `root status` after*
|
||||
- Create a status to store content (could be private) and copy its id. *Called `data status` after*
|
||||
- Reply to this status to add content. (Removing @acct, to limit self spam)
|
||||
- Reply to `root status` with options tags *(See `action status`)* like `#botodon #global #data_from_{DATA_STATUS_ID}`
|
||||
|
||||
### Botodon
|
||||
|
||||
|
@ -23,65 +25,80 @@ npm install
|
|||
cp .env.sample .env
|
||||
```
|
||||
|
||||
Edit `.env` at least set `INSTANCE=domain.tld`, `TOKEN={ACCOUNT_PRIVATE_KEY}` and `DATA_STATUS={STATUS_ID}`
|
||||
In `.env` set
|
||||
- `DOMAIN={INSTANCE_DOMAIN}`
|
||||
- `TOKEN={ACCOUNT_PRIVATE_KEY}`
|
||||
- `ROOT_STATUS={ROOT_STATUS_ID}`
|
||||
|
||||
Run `node index.js` with cron.
|
||||
Run `npm run start` with cron.
|
||||
|
||||
Enjoy
|
||||
|
||||
## Options
|
||||
## Main ideas
|
||||
|
||||
`.env` file currently contains all need options but will maybe a day be moved to mastodon statuses.
|
||||
It uses statuses tags as configuration like `#xxx_yyy`. So content doesn't really matter except obviously for `content statuses`
|
||||
|
||||
key | format | description
|
||||
There is 3 *types* of statuses:
|
||||
|
||||
- *The* `root status` contains global options. See [Root status](#root-status)
|
||||
- `action status` contains action to run and is descendant of `root status`. See [Action status](#action-status)
|
||||
- `data status` is a data source like a folder containing `content statuses`
|
||||
- `content status` is a *normal* status its content *(including medias)* will send
|
||||
|
||||
### Root status
|
||||
|
||||
Its a folder containing `action statuses` and global options as tags.
|
||||
|
||||
tag | description
|
||||
-- | --
|
||||
botodon | **Required** Enable tags processing
|
||||
async | Enable actions parallel processing
|
||||
deep | Use replies to actions as actions
|
||||
shared | **Unsafe** Include other users actions
|
||||
|
||||
### Action status
|
||||
|
||||
It describe an action to run
|
||||
|
||||
#### Sample
|
||||
|
||||
```
|
||||
#botodon #global #data_from_xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Send standard status with random content from `xxxxxxxxxxxx` status
|
||||
|
||||
#### Options
|
||||
|
||||
tag | default | description
|
||||
-- | -- | --
|
||||
INSTANCE | Domain name | Mastodon instance domain name
|
||||
TOKEN | Private key | Bot account application private key
|
||||
DATA_STATUS | Status id | Messages storage status
|
||||
VISIBILITY | [visibility](https://docs.joinmastodon.org/api/entities/#visibility) | Bot message(s) visibility
|
||||
TARGET | See [Target](#target) | Define statuses targets
|
||||
TARGET_STATUSES | Statuses id (comma separated) | Used with some `TARGET` options
|
||||
TIMEOUT | milliseconds | API timeout
|
||||
MODERATION | See [Moderation](#moderation) | Define moderation method
|
||||
MODERATION_LIMIT | int | Used with `MODERATION=favourites_count`
|
||||
botodon | false | **Required** Enable tags processing
|
||||
**Data source** | | content send
|
||||
data_from_**ID** | empty | Add a source of content: `data status` id
|
||||
data_deep | false | Use replies to content as content
|
||||
data_shared | false | **Warning** Include other users content
|
||||
data_tagged_**TAG** | empty | Require content to have this tag
|
||||
data_favourited | false | Require content to be fav by bot account
|
||||
data_favourites_**N** | false | Require at least N favourites
|
||||
data_last_**N** | disabled | Only include N last statuses
|
||||
data_weighted | false | Use `favourites_count` as promotional random weight
|
||||
data_same | false | Send same content for all targets
|
||||
**Targets** | | target accounts
|
||||
global | false | Send without target
|
||||
followers | false | Send to each followers
|
||||
followers_of_**ID** | empty | Send to each followers of given account id **Don't be evil**
|
||||
replies_to_**ID** | empty | Send to each repliers of given status id
|
||||
replies_deep | false | Include replies to replies
|
||||
replies_visibility | false | Use reply visibility
|
||||
favourites_**ID** | empty | Send ti each use who fav given status id
|
||||
visibility | unlisted | Visibility to use. One of `public`, `unlisted`, `private`, `direct`
|
||||
|
||||
Note: malformed `.env` could produce unexpected behaviors
|
||||
|
||||
### Target
|
||||
|
||||
Define statuses targets with `TARGET` option:
|
||||
|
||||
- `none`: nothing send *(default)*
|
||||
- `global`: single without target
|
||||
- `self`: single with self as target
|
||||
- `followers`: one to each follower
|
||||
- `replies`: use direct replies to `TARGET_STATUSES`
|
||||
- `replies_deep`: use deep replies to `TARGET_STATUSES`
|
||||
- `replies_smart`: same as `replies` but override `VISIBILITY with reply visibility`
|
||||
- `favourites`: use favourites of `TARGET_STATUSES`
|
||||
- `favourites_smart`: use favourites on `TARGET_STATUSES` by using tags as options
|
||||
|
||||
#### Favourites Smart
|
||||
|
||||
**WIP**
|
||||
|
||||
### Moderation
|
||||
|
||||
You could let community produce messages by making your `data status` public or unlisted.
|
||||
|
||||
For moderation, simply use `MODERATION` option:
|
||||
|
||||
- `none`: no check *(default)*
|
||||
- `self`: one your messages
|
||||
- `favourited`: bot account need to fav valid messages
|
||||
- `favourites_count`: need at least the number of fav defined with `MODERATION_LIMIT` option
|
||||
|
||||
## Multiple bots
|
||||
|
||||
Add `.env.XXX` files and run `BOT_NAME=XXX node index.js`
|
||||
|
||||
## Notes
|
||||
### Limits
|
||||
## Limits
|
||||
|
||||
Mastodon context API use an arbitrary limit of `4096` since [#7564](https://github.com/tootsuite/mastodon/pull/7564), it's so the limit were new replies are excluded.
|
||||
|
||||
|
@ -89,6 +106,8 @@ Followers soft limit of `999` *(must include pagination)* and hard limit of `750
|
|||
|
||||
## TODO
|
||||
|
||||
every thing
|
||||
add selection mode
|
||||
multiple target
|
||||
- Add abstract and inherit on deep actions
|
||||
- Add actions time validators
|
||||
- Use followers pagination
|
||||
- Add options on `data status`
|
||||
- Add move pick options
|
||||
|
|
132
index.js
132
index.js
|
@ -1,132 +0,0 @@
|
|||
const path = require('path')
|
||||
require('dotenv').config({ path: path.resolve(process.cwd(), process.env.BOT_NAME ? `.env.${process.env.BOT_NAME}` : '.env') })
|
||||
const rp = require('request-promise-native')
|
||||
|
||||
// Load env
|
||||
const ENV = {
|
||||
instance: 'INSTANCE',
|
||||
token: 'TOKEN',
|
||||
data_status_id: 'DATA_STATUS',
|
||||
data_deep: 'DATA_DEEP',
|
||||
visibility: 'VISIBILITY',
|
||||
target_mode: 'TARGET',
|
||||
target_data: 'TARGET_STATUSES',
|
||||
timeout: 'TIMEOUT',
|
||||
moderation: 'MODERATION',
|
||||
moderation_limit: 'MODERATION_LIMIT'
|
||||
}
|
||||
|
||||
function objectMap(object, mapFn) {
|
||||
return Object.keys(object).reduce(function (result, key) {
|
||||
result[key] = mapFn(object[key])
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
const options = objectMap(ENV, key => process.env[key])
|
||||
|
||||
// Setup request
|
||||
const request = rp.defaults({
|
||||
auth: {
|
||||
bearer: options.token
|
||||
},
|
||||
baseUrl: `https://${options.instance}/api/v1/`,
|
||||
timeout: options.timeout,
|
||||
json: true
|
||||
})
|
||||
|
||||
async function run(R, { data_deep, data_status_id, moderation, moderation_limit, target_mode, target_data, visibility }) {
|
||||
// verify_credentials
|
||||
const me = await R.get('accounts/verify_credentials')
|
||||
console.debug(`Logged as ${me.acct}`)
|
||||
if (!me.bot) {
|
||||
console.warn('Please set account as bot !')
|
||||
}
|
||||
|
||||
// load data
|
||||
const { descendants: dataContext } = await R.get(`statuses/${data_status_id}/context`)
|
||||
const content = dataContext.filter(s => {
|
||||
if (!data_deep && s.in_reply_to_id !== data_status_id) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (moderation) {
|
||||
case 'self':
|
||||
return s.account.id === me.id
|
||||
|
||||
case 'favourited':
|
||||
return s.favourited
|
||||
|
||||
case 'favourites_count':
|
||||
return s.favourites_count >= moderation_limit
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
console.debug(`Found ${content.length} messages (of ${content.length})`)
|
||||
if (!content.length) {
|
||||
throw 'Any message found'
|
||||
}
|
||||
|
||||
// find targets
|
||||
let targets = []
|
||||
switch (target_mode) {
|
||||
case 'global':
|
||||
targets.push({ visibility })
|
||||
break
|
||||
|
||||
case 'self':
|
||||
targets.push({ acct: me.acct, visibility })
|
||||
break
|
||||
|
||||
case 'followers':
|
||||
const followers = await R.get(`accounts/${me.id}/followers`, { qs: { limit: 999 } })
|
||||
targets.push(...followers.map(({ acct }) => ({ acct, visibility })))
|
||||
break
|
||||
|
||||
case 'replies':
|
||||
case 'replies_deep':
|
||||
case 'replies_smart':
|
||||
const allReplies = await Promise.all(target_data.split(',').map(id => {
|
||||
console.log(id)
|
||||
return R.get(`statuses/${id}/context`)
|
||||
}))
|
||||
}
|
||||
|
||||
console.log(targets)
|
||||
}
|
||||
|
||||
run(request, options)
|
||||
|
||||
/*
|
||||
Object.entries(process.env)
|
||||
.filter(e => e[0].startsWith(TOKEN))
|
||||
.forEach(e => {
|
||||
const lang = e[0].substring(TOKEN.length + 1)
|
||||
const visi = process.env[`${VISIBILITY}_${lang}`] || process.env[VISIBILITY]
|
||||
|
||||
const M = new Mastodon({
|
||||
access_token: process.env[`${TOKEN}_${lang}`] || process.env[TOKEN],
|
||||
api_url: `${process.env[`${URL}_${lang}`] || process.env[URL]}/api/v1/`,
|
||||
timeout_ms: 60 * 1000
|
||||
})
|
||||
|
||||
M.get('accounts/verify_credentials').then(
|
||||
me => {
|
||||
if (me.data.error) {
|
||||
console.error(me.data.error)
|
||||
return
|
||||
}
|
||||
|
||||
M.get(`accounts/${me.data.id}/followers`, { limit: 9999 }).then(fol => {
|
||||
for (const follow of fol.data) {
|
||||
const messages = database[Math.floor(Math.random() * database.length)]
|
||||
const text = lang.length > 0 ? messages[lang] : '\n' + Object.entries(messages).map(m => `[${m[0]}] ${m[1]}`).join('\n\n')
|
||||
|
||||
M.post('statuses', { status: `@${follow.acct} ${text}`, visibility: visi })
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
*/
|
|
@ -1,379 +0,0 @@
|
|||
{
|
||||
"name": "botodon",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
||||
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"requires": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"requires": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
|
||||
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg=="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
|
||||
"requires": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"requires": {
|
||||
"ajv": "^6.5.5",
|
||||
"har-schema": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.2.3",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.1.32",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
|
||||
"integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.0",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"request-promise-core": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
|
||||
"integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
},
|
||||
"request-promise-native": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz",
|
||||
"integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==",
|
||||
"requires": {
|
||||
"request-promise-core": "1.1.2",
|
||||
"stealthy-require": "^1.1.1",
|
||||
"tough-cookie": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
|
||||
"integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
|
||||
"requires": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
}
|
||||
},
|
||||
"stealthy-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"requires": {
|
||||
"psl": "^1.1.24",
|
||||
"punycode": "^1.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
package.json
27
package.json
|
@ -2,9 +2,17 @@
|
|||
"name": "botodon",
|
||||
"version": "0.0.1",
|
||||
"description": "A pretty simple random message bot using mastodon",
|
||||
"main": "index.js",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"start": "npm run build && npm run launch",
|
||||
"build": "tsc",
|
||||
"launch": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"lint": "tslint --project .",
|
||||
"action-add": "ts-node src/tools/action/add.ts",
|
||||
"action-clear": "ts-node src/tools/action/clear.ts",
|
||||
"action-list": "ts-node src/tools/action/list.ts",
|
||||
"data-push": "ts-node src/tools/data/push.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"mastodon",
|
||||
|
@ -15,6 +23,19 @@
|
|||
"dependencies": {
|
||||
"dotenv": "^8.0.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise-native": "^1.0.7"
|
||||
"request-promise-native": "^1.0.7",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/node": "^12.0.8",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/request-promise-native": "^1.0.16",
|
||||
"@types/winston": "^2.4.4",
|
||||
"ts-node": "^8.2.0",
|
||||
"tslint": "^5.17.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"typescript": "^3.5.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
import { VisibilityType } from 'mastodon'
|
||||
import Rest from './Rest'
|
||||
import { Action, ActionConfig, ActionConfigData, RootConfig, VISIBILITIES } from './types/config'
|
||||
import Logger from './utils/Logger'
|
||||
import matcher from './utils/matcher'
|
||||
import Selector from './utils/Selector'
|
||||
|
||||
export default class App {
|
||||
|
||||
constructor(private rest: Rest) { }
|
||||
|
||||
async run(rootId: string) {
|
||||
this.load(rootId, this.process.bind(this))
|
||||
}
|
||||
|
||||
async load(rootId: string, process: (me: string, action: Action) => Promise<any>) {
|
||||
const { id: me } = await this.login()
|
||||
|
||||
const config = await this.loadRoot(rootId, me)
|
||||
Logger.debug('Root config', config)
|
||||
if (!config.botodon) {
|
||||
Logger.error('Root status requires #botodon tag')
|
||||
return
|
||||
}
|
||||
|
||||
for (const action of await this.loadActions(rootId, config.deep, config.shared ? undefined : me)) {
|
||||
const promise = process(me, action)
|
||||
if (!config.async) {
|
||||
await promise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async login() {
|
||||
const me = await this.rest.getMe()
|
||||
Logger.debug(`Logged as ${me.acct}`)
|
||||
if (!me.bot) {
|
||||
Logger.warn('Please set account as bot !')
|
||||
}
|
||||
return me
|
||||
}
|
||||
|
||||
async loadRoot(id: string, me: string) {
|
||||
const status = await this.rest.getStatus(id)
|
||||
if (status.account.id !== me) {
|
||||
Logger.warn('Root status isn\'t yours')
|
||||
}
|
||||
|
||||
return matcher<RootConfig>({
|
||||
async: false,
|
||||
botodon: false,
|
||||
deep: false,
|
||||
shared: false
|
||||
}, status.tags.map(t => t.name).reverse())
|
||||
}
|
||||
|
||||
async loadActions(id: string, deep: boolean, account?: string) {
|
||||
const lines: Action[] = (await this.rest.getReplies(id, deep, account))
|
||||
.map(s => ({ id: s.id, tags: s.tags.map(t => t.name) }))
|
||||
|
||||
Logger.debug(`Found ${lines.length} action(s)`)
|
||||
if (!lines.length) {
|
||||
Logger.error('Root status is empty !')
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
async process(me: string, action: Action) {
|
||||
const config = matcher<ActionConfig>({
|
||||
botodon: false,
|
||||
data: {
|
||||
from: [],
|
||||
deep: false,
|
||||
shared: false,
|
||||
tagged: [],
|
||||
favourited: false,
|
||||
favourites: 0,
|
||||
last: -1,
|
||||
weighted: false,
|
||||
same: false
|
||||
},
|
||||
global: false,
|
||||
followers: false,
|
||||
followers_of: [],
|
||||
replies: {
|
||||
to: [],
|
||||
deep: false,
|
||||
visibility: false
|
||||
},
|
||||
favourites: [],
|
||||
visibility: 'unlisted'
|
||||
}, action.tags.reverse())
|
||||
|
||||
Logger.debug(`Action ${action.id} config`, config)
|
||||
if (!config.botodon) {
|
||||
Logger.error(`Action status ${action.id} requires #botodon tag`)
|
||||
return
|
||||
}
|
||||
if (!VISIBILITIES.includes(config.visibility)) {
|
||||
Logger.error(`Action status ${action.id}: invalid visibility ${config.visibility}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Data
|
||||
const datas = (await Promise.all(config.data.from.map(id => this.loadData(id, config.data, me))))
|
||||
.reduce((a, b) => a.concat(b))
|
||||
if (!datas.length) {
|
||||
Logger.error(`Action ${action.id}: Any content`)
|
||||
return
|
||||
}
|
||||
const selector = new Selector(datas, config.data.same, s => config.data.weighted ? s.favourites_count : 1)
|
||||
|
||||
// Targets
|
||||
// TODO: progressive send (limit memory usage)
|
||||
const targets: Array<{acct?: string, visibility: VisibilityType}> = []
|
||||
if (config.global) {
|
||||
targets.push({ visibility: config.visibility })
|
||||
}
|
||||
if (config.followers) {
|
||||
config.followers_of.push(me)
|
||||
}
|
||||
for await (const followers of config.followers_of.map(id => this.rest.getFollowers(id))) {
|
||||
targets.push(...followers.map(({ acct }) => ({ acct, visibility: config.visibility })))
|
||||
}
|
||||
for await (const replies of config.replies.to.map(id => this.rest.getReplies(id, config.replies.deep))) {
|
||||
targets.push(...replies.map(({ account: { acct }, visibility }) => ({
|
||||
acct, visibility: config.replies.visibility ? visibility : config.visibility
|
||||
})))
|
||||
}
|
||||
for await (const fav of config.favourites.map(id => this.rest.getFavouritedBy(id))) {
|
||||
targets.push(...fav.map(({ acct }) => ({ acct, visibility: config.visibility })))
|
||||
}
|
||||
Logger.debug(`Action ${action.id}: ${targets.length} target(s)`)
|
||||
if (!targets.length) {
|
||||
Logger.warn(`Action ${action.id}: Any target`)
|
||||
}
|
||||
|
||||
for (const target of targets) {
|
||||
const next = selector.next()
|
||||
await this.rest.postStatus({
|
||||
media_ids: next.media_attachments.map(m => m.id),
|
||||
sensitive: next.sensitive,
|
||||
spoiler_text: next.spoiler_text,
|
||||
status: `${target.acct ? `@${target.acct} ` : ''}${next.content.replace(/<[^>]*>?/gm, '')}`,
|
||||
visibility: target.visibility
|
||||
})
|
||||
}
|
||||
Logger.debug(`Action ${action.id}: done`)
|
||||
}
|
||||
|
||||
async loadData(id: string, config: ActionConfigData, me: string) {
|
||||
const replies = await this.rest.getReplies(id, config.deep, config.shared ? undefined : me)
|
||||
|
||||
return replies
|
||||
.filter(s => !config.favourited || s.favourited)
|
||||
.filter(s => s.favourites_count >= config.favourites)
|
||||
.filter(s => config.tagged.every(tag => s.tags.map(t => t.name).includes(tag)))
|
||||
.slice(0, config.last === -1 ? undefined : config.last)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { RequestPromiseAPI } from 'request-promise-native'
|
||||
import { Account, Context, Status, StatusPost } from './types/mastodon'
|
||||
|
||||
export default class Rest {
|
||||
constructor(readonly api: RequestPromiseAPI) {}
|
||||
|
||||
async getMe(): Promise<Account> {
|
||||
return this.api.get('accounts/verify_credentials')
|
||||
}
|
||||
|
||||
async getStatus(id: string): Promise<Status> {
|
||||
return this.api.get(`statuses/${id}`)
|
||||
}
|
||||
|
||||
async getContext(id: string): Promise<Context> {
|
||||
return this.api.get(`statuses/${id}/context`)
|
||||
}
|
||||
|
||||
async getDescendants(id: string) {
|
||||
return this.getContext(id)
|
||||
.then(context => context.descendants)
|
||||
}
|
||||
|
||||
async getReplies(id: string, deep: boolean, account?: string) {
|
||||
return this.getDescendants(id).then(replies => replies
|
||||
.filter(s => deep || s.in_reply_to_id === id)
|
||||
.filter(s => !account || s.account.id === account))
|
||||
}
|
||||
|
||||
async getFollowers(id: string): Promise<Account[]> {
|
||||
// TODO: use link header
|
||||
return this.api.get(`account/${id}/followers`, { qs: { limit: 999 } })
|
||||
}
|
||||
|
||||
async getFavouritedBy(id: string): Promise<Account[]> {
|
||||
// TODO: use link header
|
||||
return this.api.get(`statuses/${id}/favourited_by`, { qs: { limit: 999 } })
|
||||
}
|
||||
|
||||
async postStatus(status: StatusPost): Promise<Status> {
|
||||
return this.api.post('statuses', { body: status })
|
||||
}
|
||||
|
||||
async deleteStatus(id: string) {
|
||||
return this.api.delete(`statuses/${id}`)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import App from './App'
|
||||
import { rest, rootStatus } from './prepare'
|
||||
|
||||
const app = new App(rest)
|
||||
app.run(rootStatus)
|
|
@ -0,0 +1,17 @@
|
|||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
import rp from 'request-promise-native'
|
||||
|
||||
import Rest from './Rest'
|
||||
|
||||
dotenv.config({ path: path.resolve(process.cwd(), process.env.BOT_NAME ? `.env.${process.env.BOT_NAME}` : '.env') })
|
||||
|
||||
export const rest = new Rest(rp.defaults({
|
||||
auth: {
|
||||
bearer: process.env.TOKEN
|
||||
},
|
||||
baseUrl: `https://${process.env.DOMAIN}/api/v1/`,
|
||||
timeout: Number.parseInt(process.env.TIMEOUT, undefined),
|
||||
json: true
|
||||
}))
|
||||
export const rootStatus = process.env.ROOT_STATUS
|
|
@ -0,0 +1,18 @@
|
|||
import { rest, rootStatus } from '../../prepare'
|
||||
import Logger from '../../utils/Logger'
|
||||
|
||||
async function run() {
|
||||
const content = process.argv[2]
|
||||
if (!content) {
|
||||
Logger.error('require content')
|
||||
return
|
||||
}
|
||||
Logger.info(`adding action: "${content}"`)
|
||||
const status = await rest.postStatus({
|
||||
in_reply_to_id: rootStatus,
|
||||
status: content,
|
||||
visibility: 'private'
|
||||
})
|
||||
Logger.info('action added', { id: status.id, tags: status.tags.map(t => t.name).reverse() })
|
||||
}
|
||||
run()
|
|
@ -0,0 +1,8 @@
|
|||
import App from '../../App'
|
||||
import { rest, rootStatus } from '../../prepare'
|
||||
|
||||
async function run() {
|
||||
const app = new App(rest)
|
||||
app.load(rootStatus, (_, { id }) => rest.deleteStatus(id))
|
||||
}
|
||||
run()
|
|
@ -0,0 +1,12 @@
|
|||
import App from '../../App'
|
||||
import { rest, rootStatus } from '../../prepare'
|
||||
import Logger from '../../utils/Logger'
|
||||
|
||||
async function run() {
|
||||
const app = new App(rest)
|
||||
app.load(rootStatus, (_, action) => {
|
||||
Logger.info(JSON.stringify(action))
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
run()
|
|
@ -0,0 +1,22 @@
|
|||
import fs from 'fs'
|
||||
import { rest, rootStatus } from '../../prepare'
|
||||
import Logger from '../../utils/Logger'
|
||||
|
||||
async function run() {
|
||||
const [,, file, title] = process.argv
|
||||
if (!file) {
|
||||
Logger.error('require file')
|
||||
return
|
||||
}
|
||||
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
const ok = data.map((l: any) => Object.entries(l).map(([lang, message]) => `[${lang}] ${message}`).join('\n'))
|
||||
Logger.info('data', ok)
|
||||
/*
|
||||
const status = await rest.postStatus({
|
||||
in_reply_to_id: rootStatus,
|
||||
status: content,
|
||||
visibility: 'private'
|
||||
})
|
||||
Logger.info('action added', { id: status.id, tags: status.tags.map(t => t.name).reverse() })*/
|
||||
}
|
||||
run()
|
|
@ -0,0 +1,41 @@
|
|||
import { VisibilityType } from 'mastodon'
|
||||
|
||||
export interface RootConfig {
|
||||
botodon: boolean
|
||||
async: boolean
|
||||
deep: boolean
|
||||
shared: boolean
|
||||
}
|
||||
export interface Action {
|
||||
id: string,
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface ActionConfig {
|
||||
botodon: boolean,
|
||||
data: ActionConfigData
|
||||
global: boolean
|
||||
followers: boolean
|
||||
followers_of: string[]
|
||||
replies: {
|
||||
to: string[]
|
||||
deep: boolean
|
||||
visibility: boolean
|
||||
}
|
||||
favourites: string[]
|
||||
visibility: VisibilityType
|
||||
}
|
||||
|
||||
export interface ActionConfigData {
|
||||
from: string[]
|
||||
deep: boolean
|
||||
shared: boolean
|
||||
tagged: string[]
|
||||
favourited: boolean
|
||||
favourites: number
|
||||
last: number
|
||||
weighted: boolean
|
||||
same: boolean
|
||||
}
|
||||
|
||||
export const VISIBILITIES = ['public', 'unlisted', 'private', 'direct']
|
|
@ -0,0 +1,96 @@
|
|||
export interface Emoji {
|
||||
shortcode: string
|
||||
static_url: string
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: string
|
||||
acct: string
|
||||
bot: boolean
|
||||
display_name: string
|
||||
emojis: Emoji[]
|
||||
}
|
||||
|
||||
export type VisibilityType = 'public' | 'unlisted' | 'private' | 'direct'
|
||||
|
||||
export interface Status {
|
||||
id: string
|
||||
uri: string
|
||||
account: Account
|
||||
content: string
|
||||
created_at: string
|
||||
emojis: Emoji[]
|
||||
favourited: boolean
|
||||
favourites_count: number
|
||||
media_attachments: Media[]
|
||||
sensitive: boolean
|
||||
reblogged: boolean
|
||||
reblogs_count: number
|
||||
replies_count: number
|
||||
in_reply_to_id?: string
|
||||
reblog?: Status
|
||||
spoiler_text?: string
|
||||
card?: Card
|
||||
poll?: Poll
|
||||
tags: Tag[]
|
||||
visibility: VisibilityType
|
||||
}
|
||||
|
||||
export interface StatusPost {
|
||||
status: string
|
||||
in_reply_to_id?: string
|
||||
media_ids?: string[]
|
||||
sensitive?: boolean
|
||||
spoiler_text?: string
|
||||
visibility: VisibilityType
|
||||
}
|
||||
|
||||
export type CardType = 'link' | 'photo' | 'video' | 'rich'
|
||||
export interface Card {
|
||||
url: string
|
||||
title: string
|
||||
description: string
|
||||
image?: string
|
||||
type: CardType
|
||||
author_name?: string
|
||||
author_url?: string
|
||||
provider_name?: string
|
||||
provider_url?: string
|
||||
}
|
||||
|
||||
export interface PollOption {
|
||||
title: string
|
||||
votes_count?: number
|
||||
}
|
||||
export interface Poll {
|
||||
id: string
|
||||
expires_at?: string
|
||||
expired: boolean
|
||||
multiple: boolean
|
||||
votes_count: number
|
||||
options: PollOption[]
|
||||
voted?: boolean
|
||||
}
|
||||
|
||||
export interface PollVote {
|
||||
id: number
|
||||
poll: string
|
||||
choices: string[]
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
id: string
|
||||
description: string
|
||||
url: string
|
||||
preview_url: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
ancestors: Status[]
|
||||
descendants: Status[]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { createLogger, format, transports } from 'winston'
|
||||
const { colorize, combine, simple, prettyPrint } = format
|
||||
const { Console } = transports
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
const Logger = createLogger({
|
||||
format: combine(colorize(), simple()),
|
||||
transports: [new Console({ level: isProd ? 'warn' : 'debug' })]
|
||||
})
|
||||
|
||||
if (!isProd) {
|
||||
Logger.debug('Logging initialized at debug level')
|
||||
}
|
||||
|
||||
export default Logger
|
|
@ -0,0 +1,24 @@
|
|||
export default class Selector<T> {
|
||||
|
||||
private last: T
|
||||
private index: number[]
|
||||
|
||||
constructor(private list: T[], private same: boolean, weight: (el: T) => number) {
|
||||
this.index = list.map((s, idx) => Array(weight(s)).fill(idx)).reduce((a, b) => a.concat(b))
|
||||
}
|
||||
|
||||
next() {
|
||||
if (this.same) {
|
||||
if (this.last === undefined) {
|
||||
this.last = this.select()
|
||||
}
|
||||
return this.last
|
||||
}
|
||||
return this.select()
|
||||
}
|
||||
|
||||
private select() {
|
||||
return this.list[this.index[Math.floor(Math.random() * this.index.length)]]
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
export function match(params: any, tag: string) {
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
const value = params[key]
|
||||
if (value === false && key === tag) { // Activator
|
||||
params[key] = true
|
||||
}
|
||||
if (tag.startsWith(`${key}_`)) {
|
||||
const subTag = tag.substr(key.length + 1)
|
||||
if (Array.isArray(value)) {
|
||||
(params[key] as string[]).push(subTag)
|
||||
} else {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
params[key] = subTag
|
||||
break
|
||||
|
||||
case 'number':
|
||||
params[key] = Number(subTag)
|
||||
break
|
||||
|
||||
case 'object':
|
||||
params[key] = match(params[key], subTag)
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error('Bad type')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/** Unsafe recursive matcher */
|
||||
export default function matcher<T>(params: T, tags: string[]) {
|
||||
for (const tag of tags) {
|
||||
params = match(params, tag)
|
||||
}
|
||||
return params
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": false,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
"src/types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"defaultSeverity": "warning",
|
||||
"extends": ["tslint:recommended", "tslint-config-prettier", "tslint-plugin-prettier"],
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"node_modules/**"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"trailing-comma": false,
|
||||
"quotemark": [true, "single"],
|
||||
"indent": [true, "spaces", 2],
|
||||
"member-access": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"arrow-parens": false,
|
||||
"no-empty-interface": false,
|
||||
"interface-name": false,
|
||||
"semicolon": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue