Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
Hugh Rundle | 08c32b8647 | ||
Hugh Rundle | a85e056b4b | ||
Hugh Rundle | e2ece557c2 | ||
Hugh Rundle | 2a6cf879b6 | ||
Hugh Rundle | fa47c7a0fb | ||
Hugh Rundle | e10a3ef7ce | ||
85d6589dce | |||
Hugh Rundle | a99bd4bf92 | ||
b86560f033 | |||
Hugh Rundle | 391e7524ca | ||
Hugh Rundle | 7b9a90030a | ||
Hugh Rundle | 2bfa31907f | ||
d62209587e | |||
Hugh Rundle | d51bd3014e | ||
Hugh Rundle | c337614fe6 | ||
Hugh Rundle | e5728006bc | ||
Hugh Rundle | 0ba65f833c | ||
Hugh Rundle | 852d96f635 |
|
@ -45,6 +45,14 @@ Then run `npm install .` to install npm modules `axios` and `ws`.
|
||||||
|
|
||||||
Start the bot with the traditional `node index.js`.
|
Start the bot with the traditional `node index.js`.
|
||||||
|
|
||||||
|
However, you probably want this to run automatically on a web server. You can do this with `systemd` using the example unit file: `mastodon-clippy.service.example`. Adjust this for your user and paths, and then activate it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp mastodon-clippy.service.example /etc/systemd/system/mastodon-clippy.service
|
||||||
|
sudo systemctl enable mastodon-clippy.service
|
||||||
|
sudo systemctl start mastodon-clippy service
|
||||||
|
```
|
||||||
|
|
||||||
## interacting with the bot
|
## interacting with the bot
|
||||||
|
|
||||||
To "sign up" for notification from your bot, users have two options:
|
To "sign up" for notification from your bot, users have two options:
|
||||||
|
|
94
index.js
94
index.js
|
@ -17,7 +17,7 @@ const WebSocket = require('ws')
|
||||||
// clippy settings
|
// clippy settings
|
||||||
|
|
||||||
const access_token = process.env.CLIPPY_ACCESS_TOKEN
|
const access_token = process.env.CLIPPY_ACCESS_TOKEN
|
||||||
const topic = process.env.CLIPPY_TOPIC
|
const topic = process.env.CLIPPY_TOPIC.toLowerCase()
|
||||||
const clippy = process.env.CLIPPY_USER
|
const clippy = process.env.CLIPPY_USER
|
||||||
const domain = process.env.CLIPPY_DOMAIN
|
const domain = process.env.CLIPPY_DOMAIN
|
||||||
|
|
||||||
|
@ -26,8 +26,14 @@ const headers = {
|
||||||
'Authorization' : `Bearer ${access_token}`
|
'Authorization' : `Bearer ${access_token}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetConnection(socket) {
|
||||||
|
terminate(socket)
|
||||||
|
console.log(`waiting after error`)
|
||||||
|
setTimeout( function() { listen() }, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
// set up bot account with correct settings
|
// set up bot account with correct settings
|
||||||
function initiateSettings() {
|
function initiateSettings(socket) {
|
||||||
|
|
||||||
let account = {
|
let account = {
|
||||||
locked: false,
|
locked: false,
|
||||||
|
@ -40,31 +46,50 @@ function initiateSettings() {
|
||||||
return axios.patch(`https://${domain}/api/v1/accounts/update_credentials`, account, { headers: headers })
|
return axios.patch(`https://${domain}/api/v1/accounts/update_credentials`, account, { headers: headers })
|
||||||
.catch( err => {
|
.catch( err => {
|
||||||
console.error('ERROR applying bot user settings: ', err.message)
|
console.error('ERROR applying bot user settings: ', err.message)
|
||||||
|
terminate(socket)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function suggestion() {
|
// return random suggestion string
|
||||||
|
function suggestion(username) {
|
||||||
|
|
||||||
const n = crypto.randomInt(4)
|
const n = crypto.randomInt(12)
|
||||||
|
|
||||||
switch(n) {
|
switch(n) {
|
||||||
case 0:
|
case 0:
|
||||||
return 'How about logging off instead?';
|
return 'How about logging off instead?';
|
||||||
case 1:
|
case 1:
|
||||||
return 'Would you like to delete your toot?';
|
return `Would you like to delete your toot, @${username}?`;
|
||||||
case 2:
|
case 2:
|
||||||
return 'Can I help you take a walk outside?';
|
return 'Can I help you take a walk outside? 🚶➡️';
|
||||||
case 3:
|
case 3:
|
||||||
return 'You may like to reconsider your life choices.';
|
return 'You may like to reconsider your life choices.';
|
||||||
|
case 4:
|
||||||
|
return 'Why not try looking at #CatsOfMastodon instead?';
|
||||||
|
case 5:
|
||||||
|
return `Come on @${username}, we've talked about this. 🤷♂️`;
|
||||||
|
case 6:
|
||||||
|
return `You should go look at some trees. Trees are calming 🌳`;
|
||||||
|
case 7:
|
||||||
|
return `I'm not angry. I'm just very disappointed. 😔`;
|
||||||
|
case 8:
|
||||||
|
return `You said you were going to stop doing that ...and yet here we are.`;
|
||||||
|
case 9:
|
||||||
|
return `Time to touch some grass 🌱`;
|
||||||
|
case 10:
|
||||||
|
return `Why not have a nice cup of tea instead? 🫖`;
|
||||||
|
case 11:
|
||||||
|
return `And yet you still haven't read all of those books in your TBR pile. 🤔`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// send a message when someone toots about the topic
|
// send a message when someone toots about the topic
|
||||||
function sendResponse(rip, user) {
|
function sendResponse(rid, user, username) {
|
||||||
|
|
||||||
let payload = {
|
let payload = {
|
||||||
'status' : `@${user} It looks like you're posting about '${topic}'. ${suggestion()}`,
|
'status' : `@${user} It looks like you're posting about '${topic}'. ${suggestion(username)}`,
|
||||||
'in_reply_to_id' : rip,
|
'spoiler_text' : topic,
|
||||||
|
'in_reply_to_id' : rid,
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.post(`https://${domain}/api/v1/statuses`, payload, { headers: headers })
|
axios.post(`https://${domain}/api/v1/statuses`, payload, { headers: headers })
|
||||||
|
@ -74,6 +99,7 @@ function sendResponse(rip, user) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// follow users who subscribe
|
||||||
function followAction(id, action) {
|
function followAction(id, action) {
|
||||||
|
|
||||||
let url = `https://${domain}/api/v1/accounts/${id}/${action}`
|
let url = `https://${domain}/api/v1/accounts/${id}/${action}`
|
||||||
|
@ -90,18 +116,36 @@ function followAction(id, action) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterMentions(text, mentions) {
|
||||||
|
// filter toot text to remove mentions before checking for the trigger word
|
||||||
|
// this means if your trigger word is in a user name, you don't get a tsunami of clippy advice
|
||||||
|
let rawArray = text.replace(/(<([^>]+)>)/gi, "").split(' ')
|
||||||
|
return rawArray.map( stub => mentions.some( name => `@${name}` === stub) ? "" : stub).toString()
|
||||||
|
}
|
||||||
|
|
||||||
// ***********************
|
// ***********************
|
||||||
// STREAMING USER TIMELINE
|
// STREAMING USER TIMELINE
|
||||||
// This is where the action is!
|
// This is where the action is!
|
||||||
// ***********************
|
// ***********************
|
||||||
|
|
||||||
|
function terminate(socket) {
|
||||||
|
console.error(`Terminating connection...`)
|
||||||
|
socket.terminate()
|
||||||
|
console.log(`Terminated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function listen() {
|
||||||
|
|
||||||
|
console.log(`Listening...`)
|
||||||
const ws = new WebSocket(`wss://${domain}/api/v1/streaming?access_token=${access_token}&stream=user`)
|
const ws = new WebSocket(`wss://${domain}/api/v1/streaming?access_token=${access_token}&stream=user`)
|
||||||
|
|
||||||
// make sure bot is set up correctly each time it starts
|
// make sure bot is set up correctly each time it starts
|
||||||
initiateSettings()
|
initiateSettings(ws)
|
||||||
|
|
||||||
// errors
|
// errors
|
||||||
ws.on('error', err => {
|
ws.on('error', err => {
|
||||||
console.error(`WebSocket error: ${err.message}`)
|
console.error(`WebSocket error: ${err.message}`)
|
||||||
|
resetConnection(ws)
|
||||||
})
|
})
|
||||||
|
|
||||||
// check updates and notifications in the stream
|
// check updates and notifications in the stream
|
||||||
|
@ -135,22 +179,28 @@ ws.on('message', msg => {
|
||||||
|
|
||||||
// updates (posts)
|
// updates (posts)
|
||||||
if (packet.event == 'update') {
|
if (packet.event == 'update') {
|
||||||
let rip = data.id
|
let rid = data.id
|
||||||
let user = data.account.username
|
let user = data.account.acct
|
||||||
|
let username = data.account.username
|
||||||
// exclude own toots to avoid an infinite loop
|
// get just the account names (@name@domain.tld)
|
||||||
if (data.account.username !== clippy) {
|
let mentions = data.mentions.map( mention => mention.acct)
|
||||||
if ( data.content.includes(topic) ) {
|
// exclude own toots and @mentions to avoid an infinite loops
|
||||||
sendResponse(rip, user)
|
if (username !== clippy && !mentions.includes(clippy)) {
|
||||||
|
// get rid of mentions in case topic is within a username
|
||||||
|
let text = filterMentions(data.content, mentions)
|
||||||
|
if ( text.toLowerCase().includes(topic) ) {
|
||||||
|
sendResponse(rid, user, username)
|
||||||
}
|
}
|
||||||
else if (data.spoiler_text.includes(topic)) {
|
else if (data.spoiler_text.toLowerCase().includes(topic)) {
|
||||||
sendResponse(rip, user)
|
sendResponse(rid, user, username)
|
||||||
}
|
}
|
||||||
else if (data.tags.includes(topic)) {
|
else if (data.tags.map(tag => tag.name.toLowerCase()).includes(topic)) {
|
||||||
sendResponse(rip, user)
|
sendResponse(rid, user, username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// let's go
|
||||||
|
listen()
|
22
package-lock.json
generated
22
package-lock.json
generated
|
@ -1,26 +1,26 @@
|
||||||
{
|
{
|
||||||
"name": "mastodon-clippy",
|
"name": "mastodon-clippy",
|
||||||
"version": "1.0.1",
|
"version": "1.1.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.21.1",
|
"version": "0.21.4",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.10.0"
|
"follow-redirects": "^1.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.14.1",
|
"version": "1.15.6",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||||
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
|
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.5",
|
"version": "7.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "mastodon-clippy",
|
"name": "mastodon-clippy",
|
||||||
"version": "1.0.1",
|
"version": "1.1.2",
|
||||||
"description": "Mastodon clippy bot",
|
"description": "Mastodon clippy bot",
|
||||||
"repository": "https://github.com/hughrun/mastodon-clippy.git",
|
"repository": "https://git.suboptimal.solutions/hugh/mastodon-clippy",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
"author": "Hugh Rundle <hugh@hughrundle.net> (https://www.hughrundle.net)",
|
"author": "Hugh Rundle <hugh@hughrundle.net> (https://www.hughrundle.net)",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"axios": "^1.7.3",
|
||||||
"ws": "^7.4.5"
|
"ws": "^8.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue