Compare commits

...

93 commits

Author SHA1 Message Date
73dac81f2d
improve authorization 2024-09-29 20:53:07 +03:00
vulet
296cdc75a6 chore(package): bump version 2023-09-04 18:23:41 +08:00
vulet
4cb8d1da7e chore(help): add crossblog 2023-09-04 18:23:41 +08:00
vulet
17c7f819ac feat(fediverse): add twitter crossblog [kakashi]
chore(config): update maintained nitter instances
2023-09-04 18:23:37 +08:00
vulet
12c422c324 fix(timeline): some missing files 2023-09-04 17:43:52 +08:00
vulet
8e2ce18f26 feat(fediverse): media posting for e2ee rooms [kakashi]
feat(fediverse): direct messaging and follower-only posting [kakashi]
feat(config): emoji customization available in config [kakashi]
fix(cmd): unroll by text [kakashi]
refactor(reacts): timeline minimum +4 events => timeline minimum +1 events [kakashi]
chore(deps): upgrade matrix-js-sdk, olm, qs
chore(package): bump version
2023-08-23 15:25:48 +08:00
vulet
5924009154 feat(MSC3440): implement threads for feed and notifications.
fix(sendHtmlNotice): possibly MSC1767 related.
2022-04-26 03:09:55 +00:00
vulet
3122361c6c chore(deps): upgrade matrix-js-sdk.
fix(e2ee): getContent() only summons m.relates_to for reactions.
2022-04-25 06:53:50 +00:00
vulet
3b16a0495c feat(cmd): stop flood/notify.
fix(cmd): bad return.
chore(deps): upgrade all.
2022-01-31 18:06:01 +08:00
vulet
35cec7751f
chore(deps): bump axios from 0.21.1 to 0.21.2
chore(deps): bump axios from 0.21.1 to 0.21.2
2021-09-15 14:36:06 +08:00
dependabot[bot]
9067ae600b
chore(deps): bump axios from 0.21.1 to 0.21.2
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-15 06:35:31 +00:00
vulet
25e49ffc78
chore(deps): bump matrix-js-sdk from 12.2.0 to 12.4.1
https://github.com/vulet/ligh7hau5/issues/11

https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing

Special thanks to @otrapersona!
2021-09-15 14:35:02 +08:00
dependabot[bot]
ccdcf69bea
chore(deps): bump matrix-js-sdk from 12.2.0 to 12.4.1
Bumps [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) from 12.2.0 to 12.4.1.
- [Release notes](https://github.com/matrix-org/matrix-js-sdk/releases)
- [Changelog](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/matrix-org/matrix-js-sdk/compare/v12.2.0...v12.4.1)

---
updated-dependencies:
- dependency-name: matrix-js-sdk
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-14 20:25:01 +00:00
vulet
dded657009 chore(package): bump version. 2021-08-09 17:35:10 +08:00
vulet
06e0ce26a9 fix(archive): The latest version of the matrix-js-sdk dependency requires a Node version of at least: 12.9. Node 12 introduced TLSv1.3 as default, which axios then uses. This is causing the archive command to break, so the particular command will continue forward with TLSv1.2 for now. 2021-08-09 17:15:38 +08:00
vulet
6273452876 fix(e2ee): command-by-reaction/emote and reply by </mx-reply>.
chore(deps): upgrade matrix-js-sdk, and refactor for it. bump version.
2021-08-06 20:13:36 +08:00
vulet
44e4138b80 chore(all): bump version 2021-07-23 16:37:14 +08:00
vulet
be3e68f221 fix(nitter): missing threading.
fix(proxy): add into registrar.
2021-07-23 16:29:45 +08:00
vulet
b7e73fc533 refactor(proxy): w/ cycling, rm fallback.
refactor(config): adjust for cycling.
2021-07-23 14:02:32 +08:00
vulet
2f58d6bb84 chore(deps): update OLM dist. upgrade all. 2021-06-15 10:44:32 +08:00
vulet
472552c33b chore(cmd): add proxy shorthand 2021-06-07 15:44:21 +08:00
vulet
54631f5c2a refactor(invidious/nitter): add instance fallback. chore(readme): update instances. 2021-06-07 15:44:13 +08:00
dependabot[bot]
c7e3f26f60
chore(deps): bump ws from 7.4.3 to 7.4.6
Bumps [ws](https://github.com/websockets/ws) from 7.4.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.3...7.4.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 22:30:38 +00:00
vulet
a371b7c501 fix(e2ee): self-react after decryption promise 2021-04-25 16:04:01 +08:00
vulet
39a88b2722 fix(notifications/timeline): low lengths. chore(config): add invidious.fdn.fr. 2021-04-25 15:53:23 +08:00
vulet
340fed6346 fix(feed): attached content on mentions 2021-03-01 14:06:28 +08:00
vulet
bfde4265c6 chore(all): bump version 1.2.2 2021-02-26 13:56:40 +08:00
vulet
50e9f808da fix(fediverse): move mentions array to string. allow events for error handling. 2021-02-26 13:54:30 +08:00
vulet
dddb8ad014 chore(deps): bump versions 2021-02-26 12:32:16 +08:00
vulet
1e8577f865 feat(fediverse): automatically attach mentions to reply. refactor(mentions): clean-up. 2021-02-26 12:21:41 +08:00
vulet
f37e2471c0 fix(10grans): forecast regression 2021-02-25 16:43:08 +08:00
vulet
7a9bac2bf4 fix(10grans): don't allow rain on tipbot 2021-02-25 11:31:55 +08:00
vulet
e916778c49 refactor(10grans): adjust forecast 2021-02-23 11:07:00 +08:00
vulet
14006d9209 fix(feed): don't throw on unknown notification types. 2021-02-23 11:06:23 +08:00
vulet
58ef31356a feat(10grans): add make-it-rain 2021-02-22 17:49:26 +08:00
vulet
fcadc5addc refactor(e2ee): session management in config 2021-02-21 19:07:56 +08:00
vulet
9da2d13dbf fix(feed): use domestic homeserver 2021-02-21 14:10:32 +08:00
vulet
a9073b0b9d fix(reactions): match redaction codepoint. && fix/styling(feed): bad pathing, cleanup. 2021-02-19 14:09:40 +08:00
vulet
58fe0c19d2
Introduce meta field (1.2.0)
This introduces a `content.meta` field to our Fediverse related `m.room.message` events on Matrix. The field is attached to events that are related to notifications, and timeline. The data includes: `['status', 'reblog', 'mention', 'redact', 'unreblog', 'account']` then followed by a notice ID or an account ID. The meta field is listened for, and once encountered, a self-reaction occurs with the related available commands. After the self-reaction, we then listen for a second reaction or (`m.annotation`), and if given, act on the command specified. The commands which are currently supported include: favorite (👏), reblog (🔃), and redact (🗑). A reply is also listened for, or `m.in_reply_to`. If a reply is encountered on an event with meta, then a reply is carried out splitting at [`</mx-reply>`](https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-msgtypes) , with reaction handling done at MSC2677.
2021-02-15 00:17:35 +08:00
vulet
1e2d159053 chore(fediverse): remove old commands 2021-02-14 16:06:18 +08:00
vulet
42563ebc35 feat(matrix/fediverse): allow users to directly reply on Fediverse posts from Matrix, using <mx-reply> on our new meta field.
feat(matrix/fediverse): allow users to favorite, reblog, and redact Fediverse posts from Matrix, using m.reaction on our new meta field.
feat(fediverse): add suggestions for follow/unfollow commands.
refactor(matrix/fediverse): add meta field for commands by reaction, and commands by reply.
refactor(fediverse): relax polling on timeline/notifs thru new handling.
2021-02-14 15:57:35 +08:00
vulet
430fb350c1 fix(429): our nitter instance it too populated, switch defaults until instance bounce handling. fix(styling): use checkmark in config. 2021-02-03 12:13:05 +08:00
vulet
695a3bd0ee chore(readme): styling 2021-02-01 21:21:53 +08:00
vulet
33a262990c chore(help): update help commands 2021-02-01 17:58:17 +08:00
vulet
1a7d361b5d
Introduce OLM Encryption (1.1.0)
This release includes breaking changes. The [`node-localstorage`](https://github.com/lmaccherone/node-localstorage) package was added and has replaced our use of [`file-system`](https://github.com/douzi8/file-system).  The [`OLM`](https://gitlab.matrix.org/matrix-org/olm) package has also been added, so that we can support users who live on homeservers which require e2ee on direct messaging. The [`matrix-js-sdk`](https://github.com/matrix-org/matrix-js-sdk/) has been upgraded 7 major versions. A re-run of `yarn` or `yarn install` to re-install dependencies is required.
2021-02-01 17:42:35 +08:00
vulet
35899957eb chore(all): update for OLM refactor 2021-02-01 17:15:01 +08:00
vulet
67b88f9c96 feat(e2ee): introduce OLM encryption. refactor(storage): moved from fs to localstorage. refactor(config): added as global to registrar. chore(npm): upgraded matrix-js-sdk from 2.4.6 release to 9.5.1 release. 2021-02-01 16:58:59 +08:00
vulet
0cd373fb8a chore(help): update commands 2021-01-17 23:15:26 +08:00
vulet
ef00f3c8d5 fix(auth): pass tokens on initial startup 2021-01-17 22:29:19 +08:00
vulet
a0d7852e90 fix(ignore): move cache 2021-01-17 21:48:05 +08:00
vulet
ab86f0a3e1 refactor(registrar): add initializers 2021-01-17 21:41:11 +08:00
vulet
c1b8e37e70 chore(deps): bump versions 2021-01-17 19:36:01 +08:00
vulet
854323cbc4 chore(config): add default client name 2021-01-17 19:25:24 +08:00
vulet
65b0a07c4a chore(readme): prefer token-based authentication 2021-01-17 19:18:17 +08:00
vulet
66e751abd5 chore(ignore): add matrix/fediverse auth 2021-01-17 18:05:13 +08:00
vulet
f69b52261a refactor(all): prefer token-based authentication 2021-01-17 17:58:02 +08:00
vulet
9e17440abc feat(proxy): add more handling 2021-01-15 01:10:53 +08:00
vulet
01e43916f2 refactor(config): user formatting 2021-01-14 20:04:47 +08:00
vulet
57fe623ebe refactor(config): move subject 2021-01-14 20:00:03 +08:00
vulet
65809d235b bug(cmd): fix pathing 2021-01-14 19:03:54 +08:00
vulet
78c45451c6 refactor(config): move mimetype 2021-01-14 19:00:29 +08:00
vulet
b287d961f3 refactor(cmd): separate commands by functionality 2021-01-14 18:58:29 +08:00
vulet
cdd4429549 feat(fedi): add mxc regex for media 2021-01-14 18:52:37 +08:00
vulet
c1e8a44dd0
fix(pleroma): unicode 2021-01-01 17:30:26 +08:00
vulet
a7e32b5a3f feat(pleroma): add post visibility 2020-12-27 17:27:48 +08:00
vulet
80dcff0440 refactor(media): mimetypes 2020-11-08 14:44:00 +08:00
vulet
a94c21fdd1 refactor(fediverse): add filename to media uploads 2020-11-06 21:00:38 +08:00
vulet
f201637677 refactor(invidious/nitter): combine commands. fix(config): spelling error 2020-11-06 19:21:01 +08:00
vulet
5ed4b932d5 chore(all): bump version 2020-11-02 23:32:15 +08:00
vulet
a16da9a4cf fix(cmd): call to apply 2020-11-02 23:31:07 +08:00
vulet
2310bd29f2 chore(config): remove 2020-11-02 23:23:19 +08:00
vulet
4a4cd304df feat(nitter): add bluechecks to config. chore(config): move to config.example.js 2020-11-02 23:22:27 +08:00
vulet
beb8a4520c feat(cmd): add subjects. refactor(cmd): handling. fix(nitter): quotes. chore(config): update invidious instances. update deps. 2020-11-02 22:48:07 +08:00
vulet
a1dc8200bc feat(fediverse): add media reply 2020-09-06 23:26:45 +08:00
vulet
2e483e4744 fix(nitter): unavailable tweet error 2020-09-06 23:26:33 +08:00
vulet
292cca6f29 fix(archive): pass id 2020-07-08 04:54:05 +08:00
vulet
748ab6ca0a chore(cmd): spelling error 2020-07-04 01:13:00 +08:00
vulet
6fe3c992e7 feat(fedi): add status query 2020-07-04 01:05:39 +08:00
vulet
38ca35bdd4 fix(archive): missing id 2020-07-03 22:45:15 +08:00
vulet
59afdde7d4 feat(archive): add title to response 2020-07-03 22:16:41 +08:00
vulet
2d714943e7 feat(fedi): allow media uploads 2020-07-03 17:10:55 +08:00
vulet
402594f109 chore(docs): tidy up 2020-06-28 15:27:50 +08:00
vulet
f013eb1c98 chore(config): tidy up 2020-06-28 15:07:58 +08:00
vulet
dc0a6ff922 chore(md): prepare for release 2020-06-26 01:04:00 +08:00
vulet
f28d5655d8 fix(cmd): add WWW subdomain to whitelist 2020-06-25 20:09:07 +08:00
vulet
34b1f1b9d8 feat(cmd): Add Invidious 2020-06-25 19:32:36 +08:00
vulet
2aeea1bc37 feat(nitter): show content of quoted 2020-06-24 06:00:53 +08:00
vulet
d7396b8786 fix(nitter): error handling 2020-06-24 04:45:09 +08:00
vulet
751dbb335f feat(cmd): Add Nitter consumption 2020-06-24 04:22:30 +08:00
vulet
2d23f24a6a feat(archive.is): add rearchive, timestamp past archives 2020-06-24 02:56:08 +08:00
vulet
2c1ef158e9 fix(archive.is): event.id was nested 2020-05-27 06:36:34 +08:00
vulet
8dadc6caa4 feat(cmd): Add archive.is 2020-05-27 02:06:34 +08:00
vulet
e95cefafb9 feat(cmd): Add Mordekai 2020-05-21 07:02:27 +08:00
48 changed files with 2885 additions and 1699 deletions

10
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Patches
*.patch
# Logs
logs
*.log
@ -6,11 +9,10 @@ yarn-debug.log*
yarn-error.log*
# Ignore config
/config.js
config.js
# Ignore JSON
/timeline.json
/notification.json
# Ignore localstorage
keys
# Runtime data
pids

View file

@ -1,34 +1,47 @@
# Plemara
Plemara acts as a [Matrix](https://matrix.org/docs/spec/) bridge to the Fediverse. This application should allow you to do most actions on the Fediverse including livefeed, posting, subscribing, etc. via Matrix. Configuration for the app can be found in [config.js](https://github.com/vulet/plemara/blob/master/config.js). You will need to provide a Matrix username and password for the bridge to work, this can be done through an account made on @matrix.org, or your own homeserver. For the Fediverse side, you will need an access_token, this can be created through the CURL steps below. You would replace `fediverse.site` with where you would like to run the bridge from.
# ligh7hau5
The ligh7hau5 project is used on the Matrix protocol to communicate with the Fediverse. It is also used to proxy popular media networks(Twitter, YouTube, etc) to alternative front ends(Nitter, Invidious, etc). This repository can be ran locally, as on a RPi, or on a VPS.
# Archive (+archive URL)
This command will send a given URL to archive.is, and return an archive.is URL. This can be beneficial in two ways. One, archive.is receives your traffic instead of the URL that you wish to archive. Two, you are creating a historical context of a given URL with a dated attribute. Additionally, if there are changes that have occurred on a page, since the time of last archive, you can also use the rearchive(+rearchive URL) command. If you wish to use a different archiver, this can be configured, see the config.example.js file.
# Social Media (+proxy URL)
This command is given a Twitter or YouTube post, and then returned a respective Nitter/Invidious URL. Additionally, some data is returned about what the URL is, such as: title, description, etc. Instances can also be configured like in the above, see the config.example.js file.
# Fediverse
The ligh7hau5 works as a lite client for the Fediverse. It was built to communicate with a Pleroma instance, but it most likely works on Mastodon as well. Assuming you already have a registered account in regards to the bot, just change the config.js file and fediverse_auth.json will fill out once the bot starts.
Commands for the Fediverse include:
`+flood : turn on timeline in channel`
`+notify : show notifications in channel`
`+post <your message> : post`
`+reply <post id> <message> : reply to message`
`+media <URL> <optional message> : post media`
`+redact <post id> : delete post`
`+follow <user id> : follow`
`+unfollow <user id> : unfollow`
`+copy <post id> : repeat/repost/retweet`
`+clap <post id> : favorite`
`+boo <post id> : unfavorite`
# Installation
1. `git clone https://github.com/vulet/plemara`
2. `cd plemara && yarn install`
First, set up your config.js file, you can see config.example.js as an example. The Matrix & Fediverse login information is then used to populate keys/matrix_auth and keys/fediverse_auth during your initial login. These tokens are then used on sequential logins.
1. `git clone https://github.com/vulet/ligh7hau5`
2. `cd ligh7hau5 && yarn install`
3. `node main.js`
# Generating an access_token
1. `curl -X POST -d "client_name=<NAME HERE>&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=write follow read&website=http://fediverse.site" https://fediverse.site/api/v1/apps`
Result:
```json
{"client_id":"result",
"client_secret":"result",
"id":"result",
"name":"result",
"redirect_uri":"urn:ietf:wg:oauth:2.0:oob",
"website":"http://fediverse.site",
"vapid_key":"vapid_key"}
```
2. `curl -X POST -d "client_id=sekret&client_secret=sekret&scope=write follow read&grant_type=password&username=sekret@email.com&password=sekret" https://fediverse.site/oauth/token`
Result:
```json
{"token_type":"Bearer",
"scope":"write read",
"me":"https://fediverse.site/users/<your username>",
"access_token":"result"}
```
The access_token from the above command is then stored in the [config.js](https://github.com/vulet/plemara/blob/master/config.js) file.
# Images
![Bridge](https://civseed.com/_matrix/media/v1/download/civseed.com/wwLEtYGUUfYanovmSSAxdTJI)
# Contributors
CryptoMooners

113
auth.js Normal file
View file

@ -0,0 +1,113 @@
const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
const matrixTokenLogin = async () => {
matrixClient = sdk.createClient({
baseUrl: config.matrix.domain,
accessToken: matrix.auth.access_token,
userId: matrix.auth.user_id,
deviceId: matrix.auth.device_id,
cryptoStore: new LocalStorageCryptoStore(localStorage),
});
matrixClient.initCrypto()
.then(() => {
if (!localStorage.getItem('crypto.device_data')) {
return console.log(
'====================================================\n'
+ 'New OLM Encryption Keys created, please restart ligh7hau5.\n'
+ '====================================================',
);
}
matrixClient.setGlobalErrorOnUnknownDevices(config.matrix.manualVerify);
matrixClient.startClient();
});
};
module.exports.matrixTokenLogin = matrixTokenLogin;
module.exports.getMatrixToken = async () => {
matrixClient = sdk.createClient({ baseUrl: config.matrix.domain });
matrixClient.loginWithPassword(config.matrix.user, config.matrix.password)
.then((response) => {
matrix.auth = {
user_id: response.user_id,
access_token: response.access_token,
device_id: response.device_id,
};
localStorage.setItem('matrix_auth', JSON.stringify(response, null, 2));
}).then(() => matrixTokenLogin())
.catch((e) => {
console.log(e);
});
};
const getFediverseLink = (domain,roomId) => {
let apps = {}
apps = JSON.parse(localStorage.getItem("apps"));
if(!apps[domain]){
axios.post(`https://${domain}/api/v1/apps`,
{
client_name: config.fediverse.client_name,
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
scopes: 'read write follow push',
})
.then((response) => {
console.log(response.data)
if(!response.data.client_id || !response.data.client_secret) return false;
apps[domain] = {
client_id: response.data.client_id,
client_secret: response.data.client_secret
}
localStorage.setItem("apps",JSON.stringify(apps))
matrixClient.sendHtmlNotice(roomId,"Приложение зарегистрировано. Введите команду еще раз для создания ссылки")
// return getFediverseLink(domain)
}).catch((e) => {
console.log(e);
});
}else{
const app = apps[domain]
const uri = "urn:ietf:wg:oauth:2.0:oob".replace(/:/g,"%3A")
const scope = "read write follow push".replace(/ /g,"%20")
return `https://${domain}/oauth/authorize?client_id=${app.client_id}&response_type=code&redirect_uri=${uri}&scope=${scope}`
}
return "nothing"
};
const obtainAccessToken = (domain,code,event) => {
const apps = JSON.parse(localStorage.getItem("apps"));
console.log(domain,code)
const app = apps[domain];
axios.post(`https://${domain}/oauth/token`, {
client_id: app.client_id,
client_secret: app.client_secret,
redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
grant_type: "authorization_code",
code: code,
scopes: "read write follow push"
}).then(response => {
if(!response.data.access_token) return console.log(response.data)
fediverse.auth[event.getSender()] = {
domain: domain,
access_token: response.data.access_token
}
localStorage.setItem("fediverse_auth", JSON.stringify(fediverse.auth))
getFediverseUserInfo(event)
}).catch(e => console.error(e))
}
const getFediverseUserInfo = (event) => {
const user = event.getSender()
axios({
method: "GET",
url: `https://${fediverse.auth[user].domain}/api/v1/accounts/verify_credentials`,
headers: {
Authorization: `Bearer ${fediverse.auth[user].access_token}`
}
}).then(response => {
if(response.data.username){
matrixClient.sendHtmlNotice(event.getRoomId(), `Успешный вход в аккаунт ${response.data.display_name || response.data.username} (@${response.data.username}@${fediverse.auth[user].domain})`)
}else{
console.log(response.data)
}
}).catch(e => console.error(e))
}
module.exports.getFediverseLink = getFediverseLink;
module.exports.obtainAccessToken = obtainAccessToken;

78
commands/archive.js Normal file
View file

@ -0,0 +1,78 @@
const { JSDOM } = require('jsdom');
const qs = require('qs');
const https = require('https');
const sleep = ms => new Promise(r => setTimeout(r, ms));
const headers = ({ domain, userAgent }) => ({
'Host': `${domain}`,
'User-Agent': `${userAgent}`
});
const archive = async (instance, url, rearchive) => {
const form = await instance({ method: 'GET', url: '/' });
if (form.statusText !== 'OK') throw form;
const submitId = form.data.match(/name="submitid" value="([^"]+)/);
const submit = await instance({
method: 'POST',
url: '/submit/',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: qs.stringify({ anyway: rearchive ? '1' : undefined, submitid: submitId ? submitId[1] : undefined, url })
});
submit.title = new JSDOM(submit.data).window.document.title;
if (submit.statusText !== 'OK') throw submit;
if (submit.request.path !== '/submit/')
return { id: submit.request.path, date: submit.headers['memento-datetime'], title: submit.title };
if (submit.headers.refresh)
return { refresh: submit.headers.refresh.split(';url=')[1] };
throw submit;
};
const reqStr = str => `<em>Sending archive request for <code>${str}</code></em>`;
const arc1Str = str => `<em>Archiving page <code>${str}</code></em>`;
const arc2Str = (str, title, date) => `<em>Archived page <code><a href="https://${str}">${str}</code> [${date}]</em><br /><b>${title}</b>`;
const arc3Str = str => `<em>Timed out <code>${str}</code></em>`;
const run = async (roomId, userInput, rearchive) => {
const instance = axios.create({
baseURL: `https://${config.archive.domain}`,
httpsAgent: https.Agent({ maxVersion: "TLSv1.2"}),
headers: headers(config.archive),
transformResponse: [],
timeout: 10 * 1000
});
let reply = null;
try {
reply = await matrixClient.sendHtmlNotice(roomId, ' ', reqStr(userInput));
const { refresh, id, title, date } = await archive(instance, userInput, rearchive);
if (id)
return await matrix.utils.editNoticeHTML(roomId, reply, arc2Str(`${config.archive.domain}${id}`, title, date));
if (refresh) {
const path = refresh.split(`https://${config.archive.domain}`);
if (!path[1]) throw refresh;
await matrix.utils.editNoticeHTML(roomId, reply, arc1Str(refresh));
let tries = 30;
while (tries--) {
await sleep(10000);
const { title, date, id } = await archive(instance, userInput);
if (rearchive == false && title !== undefined)
return await matrix.utils.editNoticeHTML(roomId, reply, arc2Str(`${config.archive.domain}${id}`, title, date));
const { request: { path: reqPath }, headers: { 'memento-datetime': rearchiveDate } } = await instance({ method: 'HEAD', url: path[1] })
.catch(e => ({ request: { path: path[1] } }));
if (rearchive == true && reqPath !== path[1])
return await matrix.utils.editNoticeHTML(roomId, reply, arc2Str(`${config.archive.domain}${reqPath}`, title, rearchiveDate));
}
return await matrix.utils.editNoticeHTML(roomId, reply, arc3Str(refresh));
}
throw 'sad';
} catch (e) {
const sad = `<strong>Sad!</strong><br /><code>${`${e}`.replace(/<[^<]+?>/g, '').substr(0, 100)}</code>`;
if (reply)
matrix.utils.editNoticeHTML(roomId, reply, sad, 'sad').catch(() => {});
else
matrixClient.sendHtmlNotice(roomId, 'sad', sad).catch(() => {});
}
};
exports.runQuery = run;

View file

@ -1,21 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
data: { status: `@10grans@fedi.cc beg` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b>
<blockquote><i>You have begged for 10grans.<br>
(id: ${response.data.id}</a>)
</blockquote><br>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,18 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses/${userInput}/unfavourite`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`You have boo'd: <a href="${response.data.url}">${response.data.account.acct}</a>
<blockquote>${response.data.content}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,18 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses/${userInput}/favourite`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`You have clapped: <a href="${response.data.url}">${response.data.account.acct}</a>:
<blockquote>${response.data.content}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,18 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses/${userInput}/reblog`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`You have repeated:
<blockquote>${response.data.content}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

9
commands/expand.js Normal file
View file

@ -0,0 +1,9 @@
exports.runQuery = async (roomId, event, userInput) => {
return matrix.utils.fetchEncryptedOrNot(roomId, { event_id: userInput })
.then(event => matrix.utils.expandReact(event))
.catch(e => {
matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>')
})
.catch(() => {});
};

View file

@ -0,0 +1,12 @@
exports.runQuery = (roomId,event,userInput) => {
//matrixClient.sendHtmlNotice(roomId,"Проверка связи","Проверка связи");
const link = auth.getFediverseLink(userInput)
if(!link){
matrixClient.sendHtmlNotice(roomId,"Не удалось получить ссылку")
}else if(link == "nothing"){
}else{
authEvents.push(event.event_id)
matrixClient.sendHtmlNotice(roomId,`Перейдите по ссылке для входа в аккаунт. Для завершения ответьте на это сообщение кодом (еще не готово, пж не переходе по ссылке): ${link}`)
}
}

14
commands/fediverse/boo.js Normal file
View file

@ -0,0 +1,14 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/unfavourite`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -0,0 +1,14 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/favourite`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -0,0 +1,14 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/reblog`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -0,0 +1,39 @@
let intervalId = null;
exports.runQuery = function (roomId, disable) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (disable) return;
intervalId = setInterval(() => {
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/timelines/home`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((res) => {
let timeline = JSON.parse(localStorage.getItem('timeline'));
past = timeline[event.getSender()] || {}
if (past.length === 0) past = res.data;
const events = res.data;
const len = events.length;
for (let i = len - 1; i >= 0; i--) {
if (past.findIndex((x) => x.created_at === events[i].created_at) === -1) {
const lastStored = past.slice(past.length - 1, past.length);
if (events[i].created_at < lastStored[0].created_at) return;
events[i].label = 'status';
fediverse.utils.formatter(events[i], roomId);
}
}
timeline[event.getSender()] = events
localStorage.setItem('timeline', JSON.stringify(timeline, null, 2));
})
.catch((e) => {
matrix.utils.sendError(null, roomId, e);
});
}, 30000);
};

View file

@ -0,0 +1,19 @@
exports.runQuery = async function (roomId, event, userInput) {
const loadingString = `Searching for ${userInput}...`;
const original = await matrixClient.sendHtmlNotice(roomId, `${loadingString}`, `<code>${loadingString}</code>`);
const found = [];
const suggest = [];
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v2/search?q=${userInput}&type=accounts`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
}).then((findUserId) => {
const results = findUserId.data.accounts;
const len = results.length;
for (let i = 0; i < len; i++) results[i].acct !== userInput ? suggest.push(results[i].acct) : found.push(results[i]);
if (found.length > 0) return fediverse.utils.follow(roomId, found, event, original);
if (suggest.length > 0) msg = `<code>${userInput} was not found, suggesting:</code><blockquote>${suggest.join('<br>')}</blockquote>`;
if (suggest.length === 0) msg = `<code>No results found for: ${userInput}.</code>`;
return matrix.utils.editNoticeHTML(roomId, original, msg);
});
};

View file

@ -0,0 +1,39 @@
let intervalId = null;
exports.runQuery = function (roomId, disable) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (disable) return;
intervalId = setInterval(() => {
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/notifications`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((res) => {
let notifications = JSON.parse(localStorage.getItem('notifications'));
let past = notifications[event.getSender()] || {}
if (past.length === 0) past = res.data;
const events = res.data;
const len = events.length;
for (let i = len - 1; i >= 0; i--) {
if (past.findIndex((x) => x.created_at === events[i].created_at) === -1) {
const lastStored = past.slice(past.length - 1, past.length);
if (events[i].created_at < lastStored[0].created_at) return;
events[i].label = 'notifications';
fediverse.utils.formatter(events[i], roomId);
}
}
notifications[event.getSender()] = events
localStorage.setItem('notifications', JSON.stringify(notifications, null, 2));
})
.catch((e) => {
matrix.utils.sendError(null, roomId, e);
});
}, 30000);
};

14
commands/fediverse/pin.js Normal file
View file

@ -0,0 +1,14 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/pin`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

132
commands/fediverse/post.js Normal file
View file

@ -0,0 +1,132 @@
const qs = require('qs');
const crypto = require('crypto');
const FormData = require('form-data');
const emojis = { public: '🌐', unlisted: '📝', private: '🔒️', direct: '✉️' };
exports.visibilityEmoji = (v) => emojis[v] || v;
const mediaPathRegex = /^\/_matrix\/media\/r0\/download\/[^/]+\/[^/]+\/?$/;
const decryptMedia = (media, file) => {
const { v, key: { alg, ext, k, }, iv } = file;
if (v !== 'v2' || ext !== true || alg !== 'A256CTR')
throw new Error('Unsupported file encryption');
const key = Buffer.from(k, 'base64');
const _iv = Buffer.from(iv, 'base64');
const cipher = crypto.createDecipheriv('aes-256-ctr', key, _iv);
const data = Buffer.concat([ cipher.update(media.data), cipher.final() ]);
return Object.assign({}, media, { data });
};
const getMediaInfoFromEvent = async (roomId, event_id) => {
const event = await matrix.utils.fetchEncryptedOrNot(roomId, { event_id });
if (event.getType() !== 'm.room.message') throw new Error('Invalid type');
const content = event.getContent();
if (content.msgtype !== 'm.image') throw new Error('Invalid msgtype');
if (content.url) return { url: getMediaUrl(content.url) };
if (content.file) return {
url: getMediaUrl(content.file.url),
filename: content.body,
mimetype: content.info ? content.info.mimetype : null,
file: content.file
};
throw new Error('Invalid event');
};
const getMediaUrl = string => {
let url = new URL(string);
if (url.protocol === 'mxc:' && url.hostname && url.pathname)
url = new URL(`${config.matrix.domain}/_matrix/media/r0/download/${url.hostname}${url.pathname}`);
if (url.protocol !== 'https:' ||
!config.matrix.domains.includes(url.hostname) ||
!mediaPathRegex.test(url.pathname))
throw new Error('Invalid URL');
return url.toString();
};
const getMedia = async (roomId, string) => {
let opts = {};
if (string.startsWith('mxe://'))
opts = await getMediaInfoFromEvent(roomId, string.substring(6));
else
opts.url = getMediaUrl(string);
const media = await mediaDownload(opts);
return opts.file ? decryptMedia(media, opts.file) : media;
};
const getFilename = (header) => {
if (typeof header !== 'string') return null;
try {
const m = header.match(/inline; filename(?:=(.+)|\*=utf-8''(.+))/);
return !m ? null : m[2] && decodeURIComponent(m[2]) || m[1];
} catch (e) {
return null;
}
};
const mediaDownload = async (opts) => {
const { whitelist, blacklist } = config.fediverse.mimetypes;
const media = await axios({ method: 'GET', url: opts.url, responseType: 'arraybuffer' });
const filename = opts.filename || getFilename(media.headers['content-disposition']);
const mimetype = opts.mimetype || media.headers['content-type'];
if (media.statusText !== 'OK' || blacklist.includes(mimetype)) throw media;
if (whitelist.length && !whitelist.includes(mimetype)) throw media;
return { data: media.data, filename, mimetype };
};
const mediaUpload = async ({ domain }, { data, filename, mimetype }) => {
const form = new FormData();
form.append('file', data, {
filename: filename || 'upload',
contentType: mimetype,
});
const upload = await axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/media`,
headers: form.getHeaders({ Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` }),
data: form,
});
if (upload.statusText !== 'OK') throw upload;
return upload.data.id;
};
const run = async (roomId, event, content, replyId, mediaURL, subject, visibility) => {
let mediaId = null;
if (mediaURL) {
const media = await getMedia(roomId, mediaURL);
mediaId = await mediaUpload(config.fediverse, media);
}
if (replyId) content = await fediverse.utils.getStatusMentions(replyId, event).then(m => m.concat(content).join(' '));
const response = await axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}`, 'Content-Type': 'application/x-www-form-urlencoded' },
data: qs.stringify({
status: content,
content_type: 'text/markdown',
visibility: visibility || undefined,
media_ids: mediaURL && [mediaId] || undefined,
in_reply_to_id: replyId || undefined,
spoiler_text: subject || undefined,
}, { arrayFormat: 'brackets' }),
});
return fediverse.utils.sendEventWithMeta(roomId, `<a href="${response.data.url}">${response.data.id}</a>`, `redact ${response.data.id}`);
};
exports.runQuery = async (roomId, event, userInput, { isReply, hasMedia, hasSubject, visibility }) => {
try {
const chunks = userInput.trim().split(' ');
if (!chunks.length || chunks.length < !!isReply + !!hasMedia) throw '';
let replyId = null;
let mediaURL = null;
const subject = hasSubject ? config.fediverse.subject : null;
if (isReply) replyId = chunks.shift();
if (hasMedia) mediaURL = chunks.shift();
return await run(roomId, event, chunks.join(' '), replyId, mediaURL, subject, visibility);
} catch (e) {
console.error(e)
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
}
};

26
commands/fediverse/react.js vendored Normal file
View file

@ -0,0 +1,26 @@
const run = async (roomId, event, id, emoji, remove) => {
axios({
method: remove ? 'DELETE' : 'PUT',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/pleroma/statuses/${id}/reactions/${emoji}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};
exports.runQuery = async (roomId, event, userInput, remove) => {
try {
const chunks = userInput.trim().split(' ');
if (chunks.length !== 2) throw '';
const id = encodeURIComponent(chunks[0]);
const emoji = encodeURIComponent(chunks[1]);
return run(roomId, event, id, emoji, remove);
} catch (e) {
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
}
};

View file

@ -0,0 +1,14 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'DELETE',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -0,0 +1,15 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((response) => {
response.label = 'status';
fediverse.utils.formatter(response, roomId);
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -0,0 +1,19 @@
exports.runQuery = async function (roomId, event, userInput) {
const loadingString = `Searching for ${userInput}...`;
const original = await matrixClient.sendHtmlNotice(roomId, `${loadingString}`, `<code>${loadingString}</code>`);
const found = [];
const suggest = [];
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v2/search?q=${userInput}&type=accounts`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
}).then((findUserId) => {
const results = findUserId.data.accounts;
const len = results.length;
for (let i = 0; i < len; i++) results[i].acct !== userInput ? suggest.push(results[i].acct) : found.push(results[i]);
if (found.length > 0) return fediverse.utils.unfollow(roomId, found, event, original);
if (suggest.length > 0) msg = `<code>${userInput} was not found, suggesting:</code><blockquote>${suggest.join('<br>')}</blockquote>`;
if (suggest.length === 0) msg = `<code>No results found for: ${userInput}.</code>`;
return matrix.utils.editNoticeHTML(roomId, original, msg);
});
};

View file

@ -0,0 +1,14 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/unpin`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -0,0 +1,14 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/unreblog`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -0,0 +1,28 @@
exports.runQuery = function (roomId, event, userInput) {
const instance = axios.create({
baseURL: 'https://' + fediverse.auth[event.getSender()].domain,
method: 'GET',
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
});
instance.get(`/api/v1/statuses/${userInput}/context`)
.then(async (response) => {
let story = [];
const rel = event.getContent()['m.relates_to'];
const eventId = rel && rel.event_id ? rel.event_id : event.getId();
const original = await instance.get(`/api/v1/statuses/${userInput}`);
const ancestors = response.data.ancestors;
const descendants = response.data.descendants;
story = [...story, ancestors, original.data, descendants];
const book = story.flat();
await fediverse.utils.thread(roomId, eventId, '<br><hr><h3>...Beginning thread...</h3><hr><br>');
for (const [i, entry] of book.entries()) {
entry.label = 'thread';
fediverse.utils.formatter(entry, roomId, eventId);
}
await fediverse.utils.thread(roomId, eventId, '<br><hr><h3>...Thread ended...</h3><hr><br>');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

193
commands/fediverse/utils.js Normal file
View file

@ -0,0 +1,193 @@
const sendEventWithMeta = async (roomId, content, meta) => {
await matrixClient.sendEvent(roomId, 'm.room.message', {
body: content.replace(/<[^<]+?>/g, ''),
msgtype: 'm.notice',
formatted_body: content,
meta: meta,
format: 'org.matrix.custom.html',
});
};
const thread = async (roomId, eventId, content, meta) => {
await matrixClient.sendEvent(roomId, 'm.room.message', {
body: content.replace(/<[^<]+?>/g, ''),
msgtype: 'm.notice',
formatted_body: content,
meta: meta,
format: 'org.matrix.custom.html',
'm.relates_to': {
rel_type: 'm.thread',
event_id: eventId,
},
})
};
const hasAttachment = (res) => {
if (res.status) res = res.status;
if (!res.media_attachments) return '<br>';
return res.media_attachments.map((media) => {
const mediaURL = new URL(media.remote_url);
media.name = new URLSearchParams(mediaURL.search).get('name') || 'Unknown file name.';
return `File attachment: <a href="${media.remote_url}">${media.name}</a><br>`;
}).join('<br>');
};
const notifyFormatter = (res, roomId) => {
userDetails = `<b><a href="${config.fediverse.domain}/${res.account.id}">
${res.account.acct}</a>`;
switch (res.type) {
case 'follow':
fediverse.auth.me !== res.account.url ? res.meta = 'follow' : res.meta = 'redact';
meta = `${res.meta} ${res.account.id}`;
content = `${userDetails}
<font color="#03b381"><b>has followed you.</font>
<blockquote><i>${res.account.note}</i></blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
case 'favourite':
fediverse.auth.me !== res.account.url ? res.meta = 'favourite' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">favorited</a>
your post:</font>
<blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, res.meta);
break;
case 'mention':
fediverse.auth.me !== res.account.url ? res.meta = 'mention' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">mentioned</a>
you:</font><blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
case 'reblog':
fediverse.auth.me !== res.account.url ? res.meta = 'reblog' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">repeated</a>
your post:</font><blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
case 'pleroma:emoji_reaction':
fediverse.auth.me !== res.account.url ? res.meta = 'react' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">reacted</a> with
${ res.emoji_url ? `<a href="${res.emoji_url}">${res.emoji}</a>` : `<span>${res.emoji}</span>` }
to your post:</font><blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
default:
return console.log('Unknown notification type.');
}
};
const isOriginal = (res, roomId, event) => {
if (res.data) res = res.data;
userDetails = `<b><a href="${config.fediverse.domain}/notice/${res.id}">
${res.account.acct}</a>`;
fediverse.auth.me !== res.account.url ? res.meta = 'status' : res.meta = 'redact';
meta = `${res.meta} ${res.id}`;
content = `${userDetails}
<blockquote><i>${res.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.id}) ${registrar.post.visibilityEmoji(res.visibility)}
</blockquote>`;
if (res.label == 'thread') thread(roomId, event, content, meta);
else sendEventWithMeta(roomId, content, meta);
};
const isReblog = (res, roomId) => {
if (res.data) res = res.data;
userDetails = `<b><a href="${config.fediverse.domain}/${res.id}">
${res.account.acct}</a>`;
fediverse.auth.me !== res.account.url ? res.meta = 'status' : res.meta = 'unreblog';
meta = `${res.meta} ${res.reblog.id}`;
content = `${userDetails}
<font color="#7886D7"><b>has repeated</a>
<a href="${config.fediverse.domain}/notice/${res.reblog.id}">${res.reblog.account.acct}</a>'s post:</font>
<blockquote><i>${res.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.reblog.id}) ${registrar.post.visibilityEmoji(res.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
};
module.exports.sendEventWithMeta = sendEventWithMeta;
module.exports.thread = thread;
module.exports.formatter = (res, roomId, event) => {
const filtered = (res.label === 'notifications')
? notifyFormatter(res, roomId)
: (res.reblog == null)
? isOriginal(res, roomId, event)
: isReblog(res, roomId);
return filtered;
};
module.exports.follow = (roomId, account, event, original) => {
axios({
method: 'POST',
url: `https://${config.fediverse[event.getSender()].domain}/api/v1/accounts/${account[0].id}/follow`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
matrix.utils.editNoticeHTML(roomId, original, `<code>Followed ${account[0].acct}.</code>`);
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};
module.exports.unfollow = (roomId, account, event, original) => {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/accounts/${account[0].id}/unfollow`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
matrix.utils.editNoticeHTML(roomId, original, `<code>Unfollowed ${account[0].acct}.</code>`);
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};
module.exports.getStatusMentions = (notice, event) => {
const users = axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${notice}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
}).then((notice) => {
const users = [];
users.push('@' + notice.data.account.acct);
for(let i = 0; i < notice.data.mentions.length; i++) {
if(!config.fediverse.username.includes(notice.data.mentions[i].acct))
users.push('@' + notice.data.mentions[i].acct)
}
return users;
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
return users;
};

View file

@ -1,41 +0,0 @@
const axios = require('axios');
const fs = require('fs');
exports.runQuery = function (matrixClient, room, registrar) {
setInterval(() => {
axios({
method: 'GET',
url: `${registrar.config.fediverse}/api/v1/timelines/home`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((events) => {
const event = fs.readFileSync('timeline.json', 'utf8');
fs.writeFileSync('timeline.json', events.data[0].created_at, 'utf8');
if (event !== events.data[0].created_at) {
if (events.data[0].reblog === null) {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse}/notice/${events.data[0].id}">${events.data[0].account.acct}</a>
<blockquote><i>${events.data[0].content}<br>
${events.data[0].media_attachments.map(media =>
`<a href="${media.remote_url}">`+`${media.description}`+'</a>'
).join('<br>')}
(id: ${events.data[0].id}</a>)
</blockquote>`);
} else {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse}/${events.data[0].account.id}">
${events.data[0].account.acct}</a>
<font color="#7886D7">has <a href="${registrar.config.fediverse}/notice/${events.data[0].id}">repeated</a>:
<blockquote><a href="${events.data[0].reblog.account.url}">${events.data[0].reblog.account.acct}</a></blockquote>
<blockquote>${events.data[0].content}<br>
${events.data[0].media_attachments.map(media =>
`<a href="${media.remote_url}">`+`Proxied image, no description available.`+'</a>'
).join('<br>')}
<br>(id: ${events.data[0].id})
</blockquote>`);
}
}
});
}, 8000);
};

View file

@ -1,20 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios.get(`${registrar.config.fediverse}/api/v1/accounts/${userInput}`).then((findUID) => {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/accounts/${findUID.data.id}/follow`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
})
.then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Subscribed:
<blockquote>${registrar.config.fediverse}/${response.data.id}`);
});
}).catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,18 +1,33 @@
exports.runQuery = function (matrixClient, room) {
matrixClient.sendHtmlNotice(room.roomId,
'',
'<blockquote><b>+plemara [your message] : post<br>'
exports.runQuery = function (roomId) {
matrixClient.sendHtmlNotice(roomId,
' ',
'<blockquote><b>fediverse commands<br>'
+ '+post [your message] : post<br>'
+ '+direct [@recipient] [message] : direct message<br>'
+ '+private [message] : follower-only message<br>'
+ '+redact [post id] : delete post<br>'
+ '+fren [user id] : follow<br>'
+ '+unfren [user id] : unfollow<br>'
+ '+follow [user id] : follow<br>'
+ '+unfollow [user id] : unfollow<br>'
+ '+media [homeserver image URL or MXC] [optional message] : post media<br>'
+ '+copy [post id] : repeat/repost/retweet<br>'
+ '+crossblog [status URL]: cross blog twitter post to fediverse post<br>'
+ '+reply [post id] [content] : reply to post<br>'
+ '+tip [@user@fedi.url] [amount] : tip 10grans'
+ '+beg : beg for 10grans'
+ '+clap [post id] : favorite<br>'
+ '+boo [post id] : unfavorite</blockquote>'
+ '<blockquote><b>channel commands<br>'
+ '+flood : turn on timeline<br>'
+ '+notify : show notifications</b></blockquote>'
+ '<blockquote><b>--- <i>docs by lint</i> ---</b></blockquote>');
+ '+flood : turn on timeline in channel<br>'
+ '+notify : show notifications in channel<br>'
+ '+unflood : stop timeline in channel<br>'
+ '+unnotify : stop notifications in channel<br>'
+ '+archive [URL] : archive content<br>'
+ '+rearchive [URL] : re-archive content<br>'
+ '+nitter [status URL] : redirect twitter to nitter, also embed tweet<br>'
+ '+invidious [video URL] : redirect youtube to invidious, also embed description<br>'
+ '+proxy [twitter/youtube]: both +nitter and +invidious commands combined</b><br></blockquote>'
+ `<blockquote><b>ligh7hau5 version ${require('../package.json').version}</b><br>`
+ '<b>--- <i>Contributors🐱</i> ---</b><br>'
+ '<b>CRYPTOMOONERS</b><br>'
+ '<b>doesnm</b><br>'
+ '<b><i>docs by LINT</i></b></blockquote>'
);
};

58
commands/invidious.js Normal file
View file

@ -0,0 +1,58 @@
const invidious = async (instance, url) => {
const req = await instance({ method: 'GET', url });
if (req.statusText !== 'OK') throw req;
const { headers } = instance.defaults;
const video = JSON.parse(req.data);
return {
url: headers['Host'],
name: video.title,
date: video.publishedText,
description: video.descriptionHtml,
author: video.author,
views: video.viewCount,
likes: video.likeCount,
dislikes: video.dislikeCount,
};
};
const card = (video, path) =>
`<a href="https://${video.url}/${path}"><b>${video.name}</a></b><blockquote><b><i>` +
((video.description.length > 300) ? `${video.description.substr(0, 300)}&hellip;` : ``)+
((video.description === '<p></p>') ? `No description.`: ``)+
((video.description.length < 300 && video.description !== '<p></p>') ? `${video.description}` : ``)+
`<br /><span>🔍️ ${video.views.toLocaleString()}</span> ` +
`<span>❤️ ${video.likes.toLocaleString()}</span> ` +
`<span>❌ ${video.dislikes.toLocaleString()}</span>`+
`<br />(${video.date})</b> <br />
</blockquote>`;
const getInstance = (domain, config) =>
axios.create({
baseURL: `https://${domain}/api/v1/videos`,
headers: {
Host: `${domain}`,
'User-Agent': `${config.userAgent}`,
},
transformResponse: [],
timeout: 10 * 1000,
});
const run = async (roomId, userInput) => {
const cfg = config.invidious;
const video = await matrix.utils.retryPromise(cfg.domains.redirect, domain => invidious(getInstance(domain, cfg), userInput));
return matrixClient.sendHtmlNotice(roomId, ' ', card(video, userInput));
};
exports.runQuery = async (roomId, event, userInput) => {
try {
const url = new URL(userInput);
const { redirect, original } = config.invidious.domains;
if (!redirect.includes(url.hostname) && !original.includes(url.hostname)) throw '';
if (/^\/[\w-]{11}$/.test(url.pathname)) return await run(roomId, url.pathname.slice(1));
const params = new URLSearchParams(url.search).get('v');
if (!/^[\w-]{11}$/.test(params)) throw '';
return await run(roomId, params);
} catch (e) {
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
}
};

88
commands/nitter.js Normal file
View file

@ -0,0 +1,88 @@
const { JSDOM } = require('jsdom');
const nitter = async (instance, url) => {
const req = await instance({ method: 'GET', url });
if (req.statusText !== 'OK') throw req;
const dom = new JSDOM(req.data);
const { document } = dom.window;
const tweet = document.querySelector('#m');
const stats = tweet.querySelectorAll('.tweet-body > .tweet-stats .icon-container');
const quote = tweet.querySelector('.tweet-body > .quote');
const isReply = tweet.querySelector('.tweet-body > .replying-to');
const replies = document.querySelectorAll('.main-thread > .before-tweet > .timeline-item');
const { defaults } = instance;
return {
url: defaults.baseURL,
text: tweet.querySelector('.tweet-body > .tweet-content').innerHTML,
date: tweet.querySelector('.tweet-body > .tweet-published').textContent,
name: tweet.querySelector('.tweet-body > div .fullname').textContent,
check: !!tweet.querySelector('.tweet-body > div .fullname .icon-ok'),
handle: tweet.querySelector('.tweet-body > div .username').textContent,
hasAttachments: !!tweet.querySelector('.tweet-body > .attachments'),
quote: quote ? {
path: quote.querySelector('a.quote-link').href,
text: quote.querySelector('.quote-text') ? quote.querySelector('.quote-text').innerHTML : '',
} : null,
isReply: isReply && replies.length > 0 ? replies[replies.length - 1].classList.contains('unavailable') ? 'unavailable' : {
path: replies[replies.length - 1].querySelector('a.tweet-link').href,
text: replies[replies.length - 1].querySelector('.tweet-content').innerHTML,
} : null,
isThread: !isReply && replies.length > 0 ? replies[replies.length - 1].classList.contains('unavailable') ? 'unavailable' : {
path: replies[replies.length - 1].querySelector('a.tweet-link').href,
text: replies[replies.length - 1].querySelector('.tweet-content').innerHTML,
} : null,
stats: {
replies: stats[0].textContent.trim(),
retweets: stats[1].textContent.trim(),
favorites: stats[2].textContent.trim(),
},
};
};
const card = (tweet, check, path) =>
`<a href="${tweet.url}/${tweet.handle.replace(/^@/, '')}"><b>${tweet.name}</b></a> ` +
(tweet.check ? `${check} ` : '') +
`<a href="${tweet.url}${path}"><b>${tweet.date}</b></a> ` +
`<span>🗨️ ${tweet.stats.replies}</span> ` +
`<span>🔁 ${tweet.stats.retweets}</span> ` +
`<span>❤️ ${tweet.stats.favorites}</span> ` +
`<br /><blockquote><b><i>${tweet.text.replace('\n', '<br />')}</i></b></blockquote>` +
(tweet.hasAttachments ? '<blockquote><b>This tweet has attached media.</b></blockquote>' : '') +
(tweet.isReply ? tweet.isReply === 'unavailable' ? '<blockquote>Replied Tweet is unavailable</blockquote>' : `<blockquote><b><a href="${tweet.url}${tweet.isReply.path}">Replied Tweet</a></b><br /><b><i>${tweet.isReply.text.replace('\n', '<br />')}</i></b></blockquote>` : '') +
(tweet.isThread ? tweet.isThread === 'unavailable' ? '<blockquote>Previous Tweet is unavailable</blockquote>' : `<blockquote><b><a href="${tweet.url}${tweet.isThread.path}">Previous Tweet</a></b><br /><b><i>${tweet.isThread.text.replace('\n', '<br />')}</i></b></blockquote>` : '') +
(tweet.quote ? `<blockquote><b><a href="${tweet.url}${tweet.quote.path}">Quoted Tweet</a></b><br /><b><i>${tweet.quote.text.replace('\n', '<br />')}</i></b></blockquote>` : '');
const getInstance = (domain, config) =>
axios.create({
baseURL: `https://${domain}`,
headers: {
Host: `${domain}`,
'User-Agent': `${config.userAgent}`,
},
transformResponse: [],
timeout: 10 * 1000,
});
const run = async (roomId, userInput, fedi) => {
const cfg = config.nitter;
const tweet = await matrix.utils.retryPromise(cfg.domains.redirect, domain => nitter(getInstance(domain, cfg), userInput));
const tweetCard = card(tweet, cfg.check, userInput);
return fedi ? axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
data: { status: tweetCard, content_type: 'text/html' }
}) : matrixClient.sendHtmlNotice(roomId, ' ', tweetCard);
};
exports.runQuery = async (roomId, event, userInput, fedi) => {
try {
const url = new URL(userInput);
const { redirect, original } = config.nitter.domains;
if (!redirect.includes(url.hostname) && !original.includes(url.hostname)) throw '';
if (!/^\/[^/]+\/status\/\d+\/?$/.test(url.pathname)) throw '';
return await run(roomId, url.pathname, fedi);
} catch (e) {
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
}
};

View file

@ -1,51 +0,0 @@
const axios = require('axios');
const fs = require('fs');
exports.runQuery = function (matrixClient, room, registrar) {
setInterval(() => {
axios({
method: 'GET',
url: `${registrar.config.fediverse}/api/v1/notifications`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((notifications) => {
const event = fs.readFileSync('notification.json', 'utf8');
fs.writeFileSync('notification.json', notifications.data[0].created_at, 'utf8');
if (event !== notifications.data[0].created_at) {
if (notifications.data[0].type === 'follow') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has followed you.</b></font>
<br><i>${notifications.data[0].account.note}</i>`);
} else if (notifications.data[0].type === 'favourite') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has <a href="${notifications.data[0].status.uri}">favorited</a>
your post:</b></font>
<br><blockquote><i><b>${notifications.data[0].status.content}</i></b></blockquote>`);
} else if (notifications.data[0].type === 'mention') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has <a href="${notifications.data[0].status.uri}">mentioned</a>
you:</b></font><br><blockquote><i><b>${notifications.data[0].status.content}
<br>(id: ${notifications.data[0].status.id})</i></b>
</blockquote>`);
} else if (notifications.data[0].type === 'reblog') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has <a href="${notifications.data[0].status.uri}">repeated</a>
your post:</b></font><br>
<blockquote><i><b>${notifications.data[0].status.content}</i></b></blockquote>`);
}
}
});
}, 8000);
};

View file

@ -1,20 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses/${userInput}/pin`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Pinned:
<blockquote><i><a href="${registrar.config.fediverse}/notice/${response.data.id}">
${response.data.content}</a></i>
</blockquote>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,21 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
data: { status: userInput, content_type: `text/markdown` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b>
<blockquote><i>${response.data.content}<br>
(id: ${response.data.id}</a>)
</blockquote><br>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,17 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'DELETE',
url: `${registrar.config.fediverse}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
'<blockquote>Redacted.</blockquote');
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,18 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, address, flaggedInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
data: { status: flaggedInput, in_reply_to_id: address, content_type: `text/markdown` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`${response.data.content} ${response.data.url}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,21 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, address, flaggedInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
data: { status: `@10grans@fedi.cc tip `+ flaggedInput + ` to `+address },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b>
<blockquote><i>Tipping ${response.data.content}<br>
(id: ${response.data.id}</a>)
</blockquote><br>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,20 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios.get(`${registrar.config.fediverse}/api/v1/accounts/${userInput}`).then((findUID) => {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/accounts/${findUID.data.id}/unfollow`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
})
.then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Unsubscribed:
<blockquote>${registrar.config.fediverse}/${response.data.id}`);
});
}).catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,20 +0,0 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse}/api/v1/statuses/${userInput}/unpin`,
headers: { Authorization: `Bearer ${registrar.config.fediverseToken}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Unpinned:
<blockquote><i><a href="${registrar.config.fediverse}/notice/${response.data.id}">
${response.data.content}</a></i>
</blockquote>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,8 +0,0 @@
module.exports = {
matrixServer: 'https://server.com',
userId: '@matrixUser:server.com',
matrixUser: 'hello',
matrixPass: 'password',
fediverse: 'https://server.com',
fediverseToken: 'access_token',
};

136
main.js
View file

@ -1,110 +1,44 @@
const sdk = require('matrix-js-sdk');
const axios = require('axios');
const registrar = require('./registrar.js');
global.registrar = require('./registrar.js');
const auth = {
type: 'm.login.password',
user: registrar.config.matrixUser,
password: registrar.config.matrixPass,
};
matrix.auth.access_token ? auth.matrixTokenLogin() : auth.getMatrixToken();
//if (!fediverse.auth.access_token && config.fediverse.username) auth.registerFediverseApp();
axios.post(`${registrar.config.matrixServer}/_matrix/client/r0/login`, auth).then((response) => {
CreateClient(response.data.access_token);
}).catch((e) => {
console.log(e);
});
let CreateClient = (token) => {
const matrixClient = sdk.createClient({
baseUrl: registrar.config.matrixServer,
accessToken: token,
userId: registrar.config.userId,
timelineSupport: true,
});
matrixClient.on('RoomMember.membership', (event, member) => {
if (member.membership === 'invite' && member.userId === registrar.config.userId) {
matrixClient.joinRoom(member.roomId).done(() => {
matrixClient.on('RoomMember.membership', (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.credentials.userId) {
matrixClient.joinRoom(member.roomId).then(() => {
console.log('Auto-joined %s', member.roomId);
});
}
});
matrixClient.on('Room.timeline', (event, room, toStartOfTimeline) => {
if (toStartOfTimeline) return;
if (event.getType() !== 'm.room.message') return;
if (event.getSender() === registrar.config.userId) return;
if (member.membership === 'leave' && member.userId === matrixClient.credentials.userId) {
matrixClient.forget(member.roomId).then(() => {
console.log('Kicked %s', member.roomId);
});
}
});
matrixClient.on('event', async (event) => {
if (event.isEncrypted()) await matrixClient.decryptEventIfNeeded(event, { emit: false, isRetry: false });
if (event.getSender() === matrixClient.credentials.userId) return matrix.utils.selfReact(event);
if (!event.event.content['m.relates_to']) return;
if (event.event.unsigned.age > 10000) return;
if (event.event.content.body.charAt(0) === '+') {
console.log(`Logs: ${event.event.sender} - ${event.event.content.body}`);
const args = event.event.content.body.slice(1).trim().split(/ +/g);
return event.getType() === 'm.room.message'
? matrix.utils.handleReply(event) : matrix.utils.handleReact(event);
});
matrixClient.on('Room.timeline', async (event, member, toStartOfTimeline) => {
if (toStartOfTimeline) return;
if (event.isEncrypted()) await matrixClient.decryptEventIfNeeded(event, { emit: false, isRetry: false });
if (event.getType() !== 'm.room.message') return;
if (event.getSender() === matrixClient.credentials.userId) return;
if (event.event.unsigned.age > 10000) return;
roomId = event.event.room_id;
content = event.getContent().body;
if (!typeof content === 'string') return;
if (content.charAt(0) === '+') {
const args = content.slice(1).trim().split(/ +/g);
const command = args.shift().toLowerCase();
const userInput = args.join(' ');
const flaggedInput = userInput.substr(userInput.indexOf(' ') + 1);
const address = args.slice(0, 1).join(' ').replace(/"/g, '');
if (command === 'boo') {
registrar.boo.runQuery(matrixClient, room, userInput, registrar);
console.log(`Logs: ${event.event.sender} - ${content}`);
matrix.utils.eventHandler(args, roomId, command, event);
}
if (command === 'beg') {
registrar.beg.runQuery(matrixClient, room, registrar);
}
if (command === 'clap') {
registrar.clap.runQuery(matrixClient, room, userInput, registrar);
}
if (command === 'copy') {
registrar.copy.runQuery(matrixClient, room, userInput, registrar);
}
if (command === 'flood') {
registrar.flood.runQuery(matrixClient, room, registrar);
}
if (command === 'fren') {
registrar.fren.runQuery(matrixClient, room, userInput, registrar);
}
if (command === 'help') {
registrar.help.runQuery(matrixClient, room);
}
if (command === 'pin') {
registrar.pin.runQuery(matrixClient, room, userInput, registrar);
}
if (command === 'plemara') {
registrar.plemara.runQuery(matrixClient, room, userInput, registrar);
}
if (command === 'notify') {
registrar.notify.runQuery(matrixClient, room, registrar);
}
if (command === 'redact') {
registrar.redact.runQuery(matrixClient, room, userInput, registrar);
}
if (command === 'reply') {
registrar.reply.runQuery(matrixClient, room, address, flaggedInput, registrar);
}
if (command === 'tip') {
registrar.tip.runQuery(matrixClient, room, address, flaggedInput, registrar);
}
if (command === 'unfren') {
registrar.unfren.runQuery(matrixClient, room, userInput, registrar);
}
if (command === 'unpin') {
registrar.unpin.runQuery(matrixClient, room, userInput, registrar);
}
}
});
matrixClient.startClient();
module.exports = matrixClient;
};
});

View file

1027
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,10 @@
{
"name": "plemara",
"version": "0.2.1",
"name": "ligh7hau5",
"version": "3.0.1",
"description": "A Matrix to Fediverse client",
"engines": {
"node": ">=18.0.0"
},
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@ -9,22 +12,22 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/vulet/plemara.git"
"url": "git+https://github.com/vulet/ligh7hau5.git"
},
"author": "vul",
"license": "AGPL-3.0-only",
"bugs": {
"url": "https://github.com/vulet/plemara/issues"
"url": "https://github.com/vulet/lighthau5/issues"
},
"homepage": "https://github.com/vulet/plemara#readme",
"homepage": "https://github.com/vulet/lighthau5#readme",
"dependencies": {
"axios": "^0.19.2",
"file-system": "^2.2.2",
"matrix-js-sdk": "^2.4.6"
"axios": "^0.25.0",
"form-data": "^4.0.0",
"jsdom": "^19.0.0",
"matrix-js-sdk": "^27.2.0",
"node-localstorage": "^2.2.1",
"olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"qs": "^6.11.2"
},
"devDependencies": {
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.17.3"
}
"devDependencies": {}
}

View file

@ -1,18 +1,51 @@
global.Olm = require('olm');
global.sdk = require('matrix-js-sdk');
global.axios = require('axios');
global.config = require('./config.js');
global.auth = require('./auth.js');
global.authEvents = [];
const { LocalStorage } = require('node-localstorage');
global.localStorage = new LocalStorage('./keys');
if (!localStorage.getItem('matrix_auth')) {
localStorage.clear();
localStorage.setItem('matrix_auth', '[]');
}
if (!localStorage.getItem('fediverse_auth')) localStorage.setItem('fediverse_auth', '{}');
if (!localStorage.getItem('timeline')) localStorage.setItem('timeline', '{}');
if (!localStorage.getItem('notifications')) localStorage.setItem('notifications', '{}');
global.fediverse = {
auth: JSON.parse(localStorage.getItem('fediverse_auth')),
utils: require('./commands/fediverse/utils.js'),
};
global.matrix = {
auth: JSON.parse(localStorage.getItem('matrix_auth')),
utils: require('./utils.js'),
};
module.exports = {
config: require('./config.js'),
boo: require('./commands/boo.js'),
beg: require('./commands/beg.js'),
clap: require('./commands/clap.js'),
copy: require('./commands/copy.js'),
flood: require('./commands/flood.js'),
fren: require('./commands/fren.js'),
archive: require('./commands/archive.js'),
proxy: require('./commands/proxy.js'),
invidious: require('./commands/invidious.js'),
nitter: require('./commands/nitter.js'),
boo: require('./commands/fediverse/boo.js'),
clap: require('./commands/fediverse/clap.js'),
copy: require('./commands/fediverse/copy.js'),
flood: require('./commands/fediverse/flood.js'),
follow: require('./commands/fediverse/follow.js'),
help: require('./commands/help.js'),
pin: require('./commands/pin.js'),
plemara: require('./commands/plemara.js'),
redact: require('./commands/redact.js'),
notify: require('./commands/notify.js'),
reply: require('./commands/reply.js'),
tip: require('./commands/tip.js'),
unfren: require('./commands/unfren.js'),
unpin: require('./commands/unpin.js'),
notify: require('./commands/fediverse/notify.js'),
pin: require('./commands/fediverse/pin.js'),
post: require('./commands/fediverse/post.js'),
redact: require('./commands/fediverse/redact.js'),
status: require('./commands/fediverse/status.js'),
unfollow: require('./commands/fediverse/unfollow.js'),
unpin: require('./commands/fediverse/unpin.js'),
unreblog: require('./commands/fediverse/unreblog.js'),
unroll: require('./commands/fediverse/unroll.js'),
react: require('./commands/fediverse/react.js'),
expand: require('./commands/expand.js'),
auth: require("./commands/fediverse/auth.js")
};

View file

211
utils.js Normal file
View file

@ -0,0 +1,211 @@
const { MatrixEvent } = require('matrix-js-sdk/lib/models/event');
const url = require("url")
const isEmoji = string => true;
const sendError = async (event, roomId, e) => {
e.response ? error = `Error(${e.response.status}): ${e.response.data.error}`
: e.data ? error = `Error(${e.errcode}): ${e.data.error}`
: error = `Error: ${e.syscall}, ${e.code}`;
return matrixClient.sendHtmlNotice(roomId,
' ', error);
};
const addReact = async (event, key) => {
const roomId = event.event.room_id;
return matrixClient.sendEvent(event.event.room_id, 'm.reaction', {
'm.relates_to': {
rel_type: 'm.annotation',
event_id: event.getId(),
key,
},
}).catch((e) => sendError(null, roomId, e));
};
const eventHandler = (args, roomId, command, event) => {
const userInput = args.join(' ');
const flaggedInput = userInput.substr(userInput.indexOf(' ') + 1);
const address = args.slice(0, 1).join(' ').replace(/"/g, '');
args = [];
let visibility = null;
switch (command) {
case 'config':
return;
case 'help': case 'flood': case 'notify':
args.push(roomId);
break;
case 'unflood': case 'unnotify':
args.push(roomId, true);
command = command.substring(2);
break;
case 'unreact':
args.push(roomId, event, userInput, true);
command = 'react';
break;
case 'tip': case 'makeitrain':
args.push(roomId, event, address, flaggedInput);
break;
case 'archive': case 'rearchive':
args.push(roomId, userInput, !!~command.indexOf('re'));
command = 'archive';
break;
case 'post': case 'reply': case 'media': case 'mediareply':
case 'random': case 'randomreply': case 'randommedia': case 'randommediareply':
case 'direct': case 'directreply': case 'directmedia': case 'directmediareply':
case 'private': case 'privatereply': case 'privatemedia': case 'privatemediareply':
case 'unlisted': case 'unlistedreply': case 'unlistedmedia': case 'unlistedmediareply':
visibility = command.match(/^(direct|private|unlisted)/);
args.push(roomId, event, userInput, {
isReply: !!~command.indexOf('reply'),
hasMedia: !!~command.indexOf('media'),
hasSubject: !!~command.indexOf('random'),
visibility: visibility ? visibility[1] : null
});
command = 'post';
break;
case 'crossblog':
args.push(roomId, event, userInput, true);
command = 'nitter'
break;
// fallthrough
default:
args.push(roomId, event, userInput);
}
if(["boo","clap","copy","flood","follow","notify","pin","unpin","post","react","redact","status","unfollow","unreblog","unroll"].includes(command) && !fediverse.auth[event.getSender()]) return matrixClient.sendHtmlNotice(roomId, ' ',`${event.getSender()}, для использования команды ${command} нужно привязать аккаунт Fediverse. Используйте для этого команду +auth <имя сервера>`)
registrar[command] && registrar[command].runQuery.apply(null, args);
};
/**
matrixClient.fetchRoomEvent() does not return an Event class
however, this class is necessary for decryption, so reinstate it.
afterwards, decrypt.
*/
const fetchEncryptedOrNot = async (roomId, event) => {
const fetchedEvent = await matrixClient.fetchRoomEvent(roomId, event.event_id)
const realEvent = new MatrixEvent(fetchedEvent);
if (realEvent.isEncrypted()) {
await matrixClient.decryptEventIfNeeded(realEvent, { emit: false, isRetry: false });
}
return realEvent;
}
module.exports.sendError = sendError;
module.exports.addReact = addReact;
module.exports.eventHandler = eventHandler;
module.exports.fetchEncryptedOrNot = fetchEncryptedOrNot
module.exports.editNoticeHTML = (roomId, event, html, plain) => matrixClient.sendMessage(roomId, {
body: ` * ${plain || html.replace(/<[^<]+?>/g, '')}`,
formatted_body: ` * ${html}`,
format: 'org.matrix.custom.html',
msgtype: 'm.notice',
'm.new_content': {
body: plain || html.replace(/<[^<]+?>/g, ''),
formatted_body: html,
format: 'org.matrix.custom.html',
msgtype: 'm.notice',
},
'm.relates_to': {
rel_type: 'm.replace',
event_id: event.event_id,
},
});
module.exports.handleReact = async (event) => {
const reactions = config.matrix.reactions;
const roomId = event.event.room_id;
if (!event.getContent()['m.relates_to']) return;
const reaction = event.getContent()['m.relates_to'];
const metaEvent = await fetchEncryptedOrNot(roomId, reaction);
if (!metaEvent.getContent().meta || metaEvent.event.sender !== config.matrix.user) return;
let args = metaEvent.getContent().meta.split(' ');
isMeta = ['status', 'reblog', 'mention', 'redact', 'unreblog'];
if (!isMeta.includes(args[0])) return;
let command = [];
args.shift().toLowerCase();
switch (reaction.key) {
case reactions.copy: command = 'copy'; break;
case reactions.clap: command = 'clap'; break;
case reactions.redact: command = 'redact'; break;
case reactions.rain: command = 'makeitrain'; break;
case reactions.unroll: command = 'unroll'; break;
case reactions.expand:
command = 'expand';
args = [ reaction.event_id ];
break;
default:
if (isEmoji(reaction.key)) {
command = 'react';
args.push(reaction.key);
}
break;
}
eventHandler(args, roomId, command, event);
};
module.exports.handleReply = async (event) => {
const roomId = event.event.room_id;
if(!event.event.content['m.relates_to']['m.in_reply_to']) return;
const reply = event.event.content['m.relates_to']['m.in_reply_to'];
const metaEvent = await fetchEncryptedOrNot(roomId, reply);
if(authEvents.includes(metaEvent.event_id)){
const domain = metaEvent.event.content.body.match(/https?:\/\/[^\s]+/);
if(domain && domain[0]){
domain[0] = url.parse(domain[0]).host
let code = event.getContent().body.split("\n");
code = code[code.length-1].trim()
auth.obtainAccessToken(domain[0],code,event)
authEvents = authEvents.filter(f => f != event.event_id)
}
}
if (!metaEvent.getContent().meta || metaEvent.event.sender !== config.matrix.user) return;
const args = metaEvent.getContent().meta.split(' ');
args.push(event.getContent().formatted_body.trim().split('</mx-reply>')[1]);
isMeta = ['status', 'reblog', 'mention', 'redact', 'unreblog'];
if (!isMeta.includes(args[0])) return;
args.shift().toLowerCase();
command = 'reply';
eventHandler(args, roomId, command, event);
};
module.exports.selfReact = async (event) => {
const reactions = config.matrix.reactions;
if (event.getType() !== 'm.room.message') return;
if (event.event.unsigned.age > 10000) return;
if (!event.getContent().meta) return;
const { meta } = event.getContent();
const type = meta.split(' ')[0];
if (type === 'redact' || type === 'unreblog')
addReact(event, reactions.redact);
if (type === 'status' || type === 'reblog' || type === 'mention')
addReact(event, reactions.expand);
};
module.exports.expandReact = async (event) => {
const reactions = config.matrix.reactions;
if (event.getSender() !== matrixClient.credentials.userId) return;
if (!event.getContent().meta) return;
const { meta } = event.getContent();
const type = meta.split(' ')[0];
if (type === 'status' || type === 'reblog' || type === 'mention') {
addReact(event, reactions.unroll);
addReact(event, reactions.copy);
addReact(event, reactions.clap);
if (config.fediverse.tipping)
addReact(event, reactions.rain);
}
};
module.exports.retryPromise = async (argList, promiseFn) => {
let err;
for(var arg of argList) {
try {
return await promiseFn(arg);
} catch(e) { err = e; }
}
throw err || new Error('retryPromise error');
};

1692
yarn.lock

File diff suppressed because it is too large Load diff