archive
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": ".a2a1fb62cb588c7c72ecce0113e43445aa943172-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1590160738721,
|
||||
"name": "bridge.log.2020-05-22",
|
||||
"hash": "ee610a74498fc9ccb40f1495f9f6826b"
|
||||
}
|
||||
]
|
||||
}
|
||||
203
matrix-discord/config.yaml
Executable file
203
matrix-discord/config.yaml
Executable file
@@ -0,0 +1,203 @@
|
||||
bridge:
|
||||
# Port to host the bridge on
|
||||
# Used for communication between the homeserver and the bridge
|
||||
port: 39997
|
||||
|
||||
# The host connections to the bridge's webserver are allowed from
|
||||
bindAddress: 0.0.0.0
|
||||
|
||||
# Public domain of the homeserver
|
||||
domain: synapse.home.blapointe.com
|
||||
|
||||
# Reachable URL of the Matrix homeserver
|
||||
homeserverUrl: https://synapse.home.blapointe.com
|
||||
|
||||
# Optionally specify a different media URL used for the media store
|
||||
#
|
||||
# This is where Discord will download user profile pictures and media
|
||||
# from
|
||||
# mediaUrl: https://external-url.org
|
||||
|
||||
# Enables automatic double-puppeting when set. Automatic double-puppeting
|
||||
# allows Discord accounts to control Matrix accounts. So sending a
|
||||
# a message on Discord would send it on Matrix from your Matrix account
|
||||
#
|
||||
# loginSharedSecretMap is simply a map from homeserver URL
|
||||
# to shared secret. Example:
|
||||
#
|
||||
# loginSharedSecretMap:
|
||||
# matrix.org: "YOUR SHARED SECRET GOES HERE"
|
||||
#
|
||||
# See https://github.com/devture/matrix-synapse-shared-secret-auth for
|
||||
# the necessary server module
|
||||
# loginSharedSecretMap:
|
||||
|
||||
# Display name of the bridge bot
|
||||
displayname: Discord Puppet Bridge
|
||||
|
||||
# Avatar URL of the bridge bot
|
||||
# avatarUrl: mxc://example.com/abcdef12345
|
||||
|
||||
# Whether to create groups for each Discord Server
|
||||
#
|
||||
# Note that 'enable_group_creation' must be 'true' in Synapse's config
|
||||
# for this to work
|
||||
enableGroupSync: true
|
||||
|
||||
presence:
|
||||
# Bridge Discord online/offline status
|
||||
enabled: true
|
||||
|
||||
# How often to send status to the homeserver in milliseconds
|
||||
interval: 30000
|
||||
|
||||
provisioning:
|
||||
# Regex of Matrix IDs allowed to use the puppet bridge
|
||||
whitelist:
|
||||
# Allow a specific user
|
||||
# - "@user:server\\.com"
|
||||
|
||||
# Allow users on a specific homeserver
|
||||
- "@.*:synapse.home.blapointe.com"
|
||||
|
||||
# Allow anyone
|
||||
# - ".*"
|
||||
|
||||
# Regex of Matrix IDs forbidden from using the puppet bridge
|
||||
# blacklist:
|
||||
# Disallow a specific user
|
||||
# - "@user:server\\.com"
|
||||
|
||||
# Disallow users on a specific homeserver
|
||||
# - "@.*:server\\.com"
|
||||
|
||||
relay:
|
||||
# Regex of Matrix IDs who are allowed to use the bridge in relay mode.
|
||||
# Relay mode is when a single Discord bot account relays messages of
|
||||
# multiple Matrix users
|
||||
#
|
||||
# Same format as in provisioning
|
||||
whitelist:
|
||||
- "@.*:synapse.home.blapointe.com"
|
||||
|
||||
# blacklist:
|
||||
# - "@user:yourserver\\.com"
|
||||
|
||||
selfService:
|
||||
# Regex of Matrix IDs who are allowed to use bridge self-servicing (plumbed rooms)
|
||||
#
|
||||
# Same format as in provisioning
|
||||
whitelist:
|
||||
- "@.*:synapse.home.blapointe.com"
|
||||
|
||||
# blacklist:
|
||||
# - "@user:server\\.com"
|
||||
|
||||
# Map of homeserver URLs to their C-S API endpoint
|
||||
#
|
||||
# Useful for double-puppeting if .well-known is unavailable for some reason
|
||||
# TODO?
|
||||
# homeserverUrlMap:
|
||||
# yourserver.com: http://localhost:1234
|
||||
|
||||
# Override the default name patterns for users, rooms and groups
|
||||
#
|
||||
# Variable names must be prefixed with a ':'
|
||||
namePatterns:
|
||||
# The default displayname for a bridged user
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: username of the user
|
||||
# discriminator: hashtag of the user (ex. #1234)
|
||||
user: :name:discriminator
|
||||
|
||||
# A user's guild-specific displayname - if they've set a custom nick in
|
||||
# a guild
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: username of the user
|
||||
# discriminator: hashtag of the user (ex. #1234)
|
||||
# displayname: the user's custom group-specific nick
|
||||
# channel: the name of the channel
|
||||
# guild: the name of the guild
|
||||
userOverride: :displayname/:name:discriminator
|
||||
|
||||
# Room names for bridged Discord channels
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: name of the channel
|
||||
# guild: name of the guild
|
||||
room: :name
|
||||
|
||||
# Group names for bridged Discord servers
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: name of the guide
|
||||
group: d.:name
|
||||
|
||||
database:
|
||||
# Use Postgres as a database backend. If set, will be used instead of SQLite3
|
||||
#
|
||||
# Connection string to connect to the Postgres instance
|
||||
# with username "user", password "pass", host "localhost" and database name "dbname".
|
||||
#
|
||||
# Modify each value as necessary
|
||||
# connString: "postgres://user:pass@localhost/dbname?sslmode=disable"
|
||||
|
||||
# Use SQLite3 as a database backend
|
||||
#
|
||||
# The name of the database file
|
||||
filename: database.db
|
||||
|
||||
limits:
|
||||
# Up to how many users should be auto-joined on room creation? -1 to disable
|
||||
# auto-join functionality
|
||||
#
|
||||
# Defaults to 200
|
||||
# maxAutojoinUsers: 200
|
||||
|
||||
# How long the delay between two auto-join users should be in milliseconds
|
||||
#
|
||||
# Defaults to 5000
|
||||
# roomUserAutojoinDelay: 5000
|
||||
|
||||
logging:
|
||||
# Log level of console output
|
||||
#
|
||||
# Allowed values starting with most verbose:
|
||||
# silly, verbose, info, warn, error
|
||||
console: info
|
||||
|
||||
# Date and time formatting
|
||||
lineDateFormat: MMM-D HH:mm:ss.SSS
|
||||
|
||||
# Logging files
|
||||
#
|
||||
# Log files are rotated daily by default
|
||||
files:
|
||||
# Log file path
|
||||
- file: "bridge.log"
|
||||
# Log level for this file
|
||||
#
|
||||
# Allowed values starting with most verbose:
|
||||
# silly, debug, verbose, info, warn, error
|
||||
level: info
|
||||
|
||||
# Date and time formatting
|
||||
datePattern: YYYY-MM-DD
|
||||
|
||||
# Maximum number of logs to keep.
|
||||
#
|
||||
# This can be a number of files or number of days.
|
||||
# If using days, add 'd' as a suffix
|
||||
maxFiles: 14d
|
||||
|
||||
# Maximum size of the file after which it will rotate.
|
||||
# This can be a number of bytes, or units of kb, mb, and gb.
|
||||
|
||||
# If using units, add 'k', 'm', or 'g' as the suffix
|
||||
maxSize: 50m
|
||||
15
matrix-discord/discord.yaml
Executable file
15
matrix-discord/discord.yaml
Executable file
@@ -0,0 +1,15 @@
|
||||
as_token: 9ea6bc07-9bc0-4af2-a738-1312519e5625
|
||||
hs_token: 9fce3f39-7fd5-493e-81d1-4373d4efbe9d
|
||||
id: discord-puppet
|
||||
namespaces:
|
||||
users:
|
||||
- exclusive: true
|
||||
regex: '@_discordpuppet_.*'
|
||||
rooms: []
|
||||
aliases:
|
||||
- exclusive: true
|
||||
regex: '#_discordpuppet_.*'
|
||||
protocols: []
|
||||
rate_limited: false
|
||||
sender_localpart: _discordpuppet_bot
|
||||
url: 'http://matrix-discord.scratch.com'
|
||||
BIN
matrix-discord/master.zip
Executable file
BIN
matrix-discord/master.zip
Executable file
Binary file not shown.
5
matrix-discord/mx-puppet-discord/.gitignore
vendored
Executable file
5
matrix-discord/mx-puppet-discord/.gitignore
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
config.yaml
|
||||
discord-registration.yaml
|
||||
node_modules
|
||||
build
|
||||
*.db
|
||||
39
matrix-discord/mx-puppet-discord/Dockerfile
Executable file
39
matrix-discord/mx-puppet-discord/Dockerfile
Executable file
@@ -0,0 +1,39 @@
|
||||
FROM node:latest AS builder
|
||||
|
||||
WORKDIR /opt/mx-puppet-discord
|
||||
|
||||
RUN apt update && apt -y install ca-certificates
|
||||
|
||||
# run build process as user in case of npm pre hooks
|
||||
# pre hooks are not executed while running as root
|
||||
RUN chown node:node /opt/mx-puppet-discord
|
||||
USER node
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY tsconfig.json ./
|
||||
COPY src/ ./src/
|
||||
RUN npm run build
|
||||
|
||||
|
||||
#FROM node:alpine
|
||||
|
||||
VOLUME /data
|
||||
|
||||
ENV CONFIG_PATH=/data/config.yaml \
|
||||
REGISTRATION_PATH=/data/discord-registration.yaml
|
||||
|
||||
# su-exec is used by docker-run.sh to drop privileges
|
||||
#RUN apk add --no-cache su-exec
|
||||
|
||||
WORKDIR /opt/mx-puppet-discord
|
||||
COPY docker-run.sh ./
|
||||
#COPY /opt/mx-puppet-discord/node_modules/ ./node_modules/
|
||||
#COPY /opt/mx-puppet-discord/build/ ./build/
|
||||
|
||||
# change workdir to /data so relative paths in the config.yaml
|
||||
# point to the persisten volume
|
||||
WORKDIR /mnt
|
||||
|
||||
ENTRYPOINT ["/opt/mx-puppet-discord/docker-run.sh"]
|
||||
201
matrix-discord/mx-puppet-discord/LICENSE
Executable file
201
matrix-discord/mx-puppet-discord/LICENSE
Executable file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
127
matrix-discord/mx-puppet-discord/README.md
Executable file
127
matrix-discord/mx-puppet-discord/README.md
Executable file
@@ -0,0 +1,127 @@
|
||||
[](https://matrix.to/#/#mx-puppet-bridge:sorunome.de) [](https://liberapay.com/Sorunome/donate)
|
||||
|
||||
# mx-puppet-discord
|
||||
This is a discord puppeting bridge for matrix. It handles bridging private and group DMs, as well as Guilds (servers).
|
||||
It is based on [mx-puppet-bridge](https://github.com/Sorunome/mx-puppet-bridge).
|
||||
|
||||
Also see [matrix-appservice-discord](https://github.com/Half-Shot/matrix-appservice-discord) for an alternative guild-only bridge.
|
||||
|
||||
## Setup
|
||||
|
||||
Clone the repo and install the dependencies:
|
||||
|
||||
```
|
||||
git clone https://github.com/matrix-discord/mx-puppet-discord
|
||||
cd mx-puppet-discord
|
||||
npm install
|
||||
```
|
||||
|
||||
Copy and edit the configuration file to your liking:
|
||||
|
||||
```
|
||||
cp sample.config.yaml config.yaml
|
||||
... edit config.yaml ...
|
||||
```
|
||||
|
||||
Generate an appservice registration file. Optional parameters are shown in
|
||||
brackets with default values:
|
||||
|
||||
```
|
||||
npm run start -- -r [-c config.yaml] [-f discord-registration.yaml]
|
||||
```
|
||||
|
||||
Then add the path to the registration file to your synapse `homeserver.yaml`
|
||||
under `app_service_config_files`, and restart synapse.
|
||||
|
||||
Finally, run the bridge:
|
||||
|
||||
```
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Start a chat with `@_discordpuppet_bot:yourserver.com`. When it joins, type
|
||||
`help` in the chat to see instructions.
|
||||
|
||||
### Linking a Discord bot account
|
||||
|
||||
This is the recommended method, and allows Discord users to PM you through a
|
||||
bot.
|
||||
|
||||
First visit your [Discord Application
|
||||
Portal](https://discordapp.com/login?redirect_to=%2Fdevelopers%2Fapplications%2Fme).
|
||||
|
||||
1. Click on 'New Application'
|
||||
|
||||

|
||||
|
||||
2. Customize your bot how you like
|
||||
|
||||

|
||||
|
||||
3. Go to ‘**Create Application**’ and scroll down to the next page. Find ‘**Create a Bot User**’ and click on it.
|
||||
|
||||

|
||||
|
||||
4. Click '**Yes, do it!**
|
||||
|
||||

|
||||
|
||||
5. Find the bot's token in the '**App Bot User**' section.
|
||||
|
||||

|
||||
|
||||
6. Click '**Click to Reveal**'
|
||||
|
||||

|
||||
|
||||
Finally, send the appservice bot a message with the contents `link bot
|
||||
your.token-here`.
|
||||
|
||||
### Linking your Discord account
|
||||
|
||||
**Warning**: Linking your user account's token is against Discord's Terms of Service.
|
||||
|
||||
First [retrieve your Discord User Token](https://discordhelp.net/discord-token).
|
||||
|
||||
Then send the bot a message with the contents `link user your.token-here`.
|
||||
|
||||
### Guild management
|
||||
|
||||
As most users are in many guilds none are bridged by default. You can, however, enable bridging a guild. For that use `listguilds <puppetId>`, e.g. `listguilds 1`. (Puppet ID can be found with `list`.)
|
||||
|
||||
Then, to bridge a guild, type `bridgeguild <puppetId> <guildId>` and to unbridge it type `unbridgeguild <puppetId> <guildId>`
|
||||
|
||||
### Friends management
|
||||
|
||||
**IMPORTANT! This is a USER-token ONLY feature, and as such against discords TOS. When developing this test-accounts got softlocked, USE AT YOUR OWN RISK!**
|
||||
|
||||
You first need to enable friends management with `enablefriendsmanagement <puppetId>`.
|
||||
|
||||
You can view all friends and invitation status with `listfriends <puppetId>`.
|
||||
|
||||
You can accept a friends request / send a friends request with `addfriend <puppetId> <user>` where `<user>` is either the user ID (preferred) or the `username#1234`.
|
||||
|
||||
You can remove friends with `removefriend <puppetId> <userId>`.
|
||||
|
||||
## Docker
|
||||
|
||||
Build docker image:
|
||||
|
||||
docker build -t mx-puppet-discord .
|
||||
|
||||
You may want some changes in your config.yaml:
|
||||
|
||||
```yaml
|
||||
bindAddress: 0.0.0.0
|
||||
filename: '/data/database.db'
|
||||
file: '/data/bridge.log'
|
||||
```
|
||||
|
||||
Once the bridge has generated the `discord-registration.yaml` edit it to fix the
|
||||
address so that your matrix home server can connect to the bridge:
|
||||
|
||||
```yaml
|
||||
url: 'http://discord:8434'
|
||||
```
|
||||
38
matrix-discord/mx-puppet-discord/docker-run.sh
Executable file
38
matrix-discord/mx-puppet-discord/docker-run.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
if [ ! -f "$CONFIG_PATH" ]; then
|
||||
echo 'No config found'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
args="$@"
|
||||
|
||||
if [ ! -f "$REGISTRATION_PATH" ]; then
|
||||
echo 'No registration found, generating now'
|
||||
args="-r"
|
||||
fi
|
||||
|
||||
|
||||
# if no --uid is supplied, prepare files to drop privileges
|
||||
if [ "$(id -u)" = 0 ]; then
|
||||
chown node:node /data
|
||||
|
||||
if find *.db > /dev/null 2>&1; then
|
||||
# make sure sqlite files are writeable
|
||||
chown node:node *.db
|
||||
fi
|
||||
if find *.log.* > /dev/null 2>&1; then
|
||||
# make sure log files are writeable
|
||||
chown node:node *.log.*
|
||||
fi
|
||||
|
||||
su_exec='su-exec node:node'
|
||||
else
|
||||
su_exec=''
|
||||
fi
|
||||
|
||||
# $su_exec is used in case we have to drop the privileges
|
||||
exec $su_exec /usr/local/bin/node '/opt/mx-puppet-discord/build/index.js' \
|
||||
-c "$CONFIG_PATH" \
|
||||
-f "$REGISTRATION_PATH" \
|
||||
$args
|
||||
BIN
matrix-discord/mx-puppet-discord/img/bot-1.jpg
Executable file
BIN
matrix-discord/mx-puppet-discord/img/bot-1.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
matrix-discord/mx-puppet-discord/img/bot-2.jpg
Executable file
BIN
matrix-discord/mx-puppet-discord/img/bot-2.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
matrix-discord/mx-puppet-discord/img/bot-3.jpg
Executable file
BIN
matrix-discord/mx-puppet-discord/img/bot-3.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
matrix-discord/mx-puppet-discord/img/bot-4.jpg
Executable file
BIN
matrix-discord/mx-puppet-discord/img/bot-4.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
matrix-discord/mx-puppet-discord/img/bot-5.jpg
Executable file
BIN
matrix-discord/mx-puppet-discord/img/bot-5.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
matrix-discord/mx-puppet-discord/img/bot-6.jpg
Executable file
BIN
matrix-discord/mx-puppet-discord/img/bot-6.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
31
matrix-discord/mx-puppet-discord/package.json
Executable file
31
matrix-discord/mx-puppet-discord/package.json
Executable file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "mx-puppet-discord",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "tslint --project ./tsconfig.json -t stylish",
|
||||
"start": "npm run-script build && node ./build/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Sorunome",
|
||||
"dependencies": {
|
||||
"better-discord.js": "git+https://github.com/Sorunome/better-discord.js.git#bb8be2aad13daf862d971119ab2441ce2219fc94",
|
||||
"command-line-args": "^5.1.1",
|
||||
"command-line-usage": "^5.0.5",
|
||||
"escape-html": "^1.0.3",
|
||||
"events": "^3.0.0",
|
||||
"expire-set": "^1.0.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"matrix-discord-parser": "0.0.11",
|
||||
"mime": "^2.4.4",
|
||||
"mx-puppet-bridge": "0.0.43-2",
|
||||
"path": "^0.12.7",
|
||||
"tslint": "^5.17.0",
|
||||
"typescript": "^3.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.8"
|
||||
}
|
||||
}
|
||||
202
matrix-discord/mx-puppet-discord/sample.config.yaml
Executable file
202
matrix-discord/mx-puppet-discord/sample.config.yaml
Executable file
@@ -0,0 +1,202 @@
|
||||
bridge:
|
||||
# Port to host the bridge on
|
||||
# Used for communication between the homeserver and the bridge
|
||||
port: 8434
|
||||
|
||||
# The host connections to the bridge's webserver are allowed from
|
||||
bindAddress: localhost
|
||||
|
||||
# Public domain of the homeserver
|
||||
domain: matrix.org
|
||||
|
||||
# Reachable URL of the Matrix homeserver
|
||||
homeserverUrl: https://matrix.org
|
||||
|
||||
# Optionally specify a different media URL used for the media store
|
||||
#
|
||||
# This is where Discord will download user profile pictures and media
|
||||
# from
|
||||
#mediaUrl: https://external-url.org
|
||||
|
||||
# Enables automatic double-puppeting when set. Automatic double-puppeting
|
||||
# allows Discord accounts to control Matrix accounts. So sending a
|
||||
# a message on Discord would send it on Matrix from your Matrix account
|
||||
#
|
||||
# loginSharedSecretMap is simply a map from homeserver URL
|
||||
# to shared secret. Example:
|
||||
#
|
||||
# loginSharedSecretMap:
|
||||
# matrix.org: "YOUR SHARED SECRET GOES HERE"
|
||||
#
|
||||
# See https://github.com/devture/matrix-synapse-shared-secret-auth for
|
||||
# the necessary server module
|
||||
#loginSharedSecretMap:
|
||||
|
||||
# Display name of the bridge bot
|
||||
displayname: Discord Puppet Bridge
|
||||
|
||||
# Avatar URL of the bridge bot
|
||||
#avatarUrl: mxc://example.com/abcdef12345
|
||||
|
||||
# Whether to create groups for each Discord Server
|
||||
#
|
||||
# Note that 'enable_group_creation' must be 'true' in Synapse's config
|
||||
# for this to work
|
||||
enableGroupSync: true
|
||||
|
||||
presence:
|
||||
# Bridge Discord online/offline status
|
||||
enabled: true
|
||||
|
||||
# How often to send status to the homeserver in milliseconds
|
||||
interval: 500
|
||||
|
||||
provisioning:
|
||||
# Regex of Matrix IDs allowed to use the puppet bridge
|
||||
whitelist:
|
||||
# Allow a specific user
|
||||
#- "@user:server\\.com"
|
||||
|
||||
# Allow users on a specific homeserver
|
||||
- "@.*:server\\.com"
|
||||
|
||||
# Allow anyone
|
||||
#- ".*"
|
||||
|
||||
# Regex of Matrix IDs forbidden from using the puppet bridge
|
||||
#blacklist:
|
||||
# Disallow a specific user
|
||||
#- "@user:server\\.com"
|
||||
|
||||
# Disallow users on a specific homeserver
|
||||
#- "@.*:server\\.com"
|
||||
|
||||
relay:
|
||||
# Regex of Matrix IDs who are allowed to use the bridge in relay mode.
|
||||
# Relay mode is when a single Discord bot account relays messages of
|
||||
# multiple Matrix users
|
||||
#
|
||||
# Same format as in provisioning
|
||||
whitelist:
|
||||
- "@.*:yourserver\\.com"
|
||||
|
||||
#blacklist:
|
||||
#- "@user:yourserver\\.com"
|
||||
|
||||
selfService:
|
||||
# Regex of Matrix IDs who are allowed to use bridge self-servicing (plumbed rooms)
|
||||
#
|
||||
# Same format as in provisioning
|
||||
whitelist:
|
||||
- "@.*:server\\.com"
|
||||
|
||||
#blacklist:
|
||||
#- "@user:server\\.com"
|
||||
|
||||
# Map of homeserver URLs to their C-S API endpoint
|
||||
#
|
||||
# Useful for double-puppeting if .well-known is unavailable for some reason
|
||||
#homeserverUrlMap:
|
||||
#yourserver.com: http://localhost:1234
|
||||
|
||||
# Override the default name patterns for users, rooms and groups
|
||||
#
|
||||
# Variable names must be prefixed with a ':'
|
||||
namePatterns:
|
||||
# The default displayname for a bridged user
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: username of the user
|
||||
# discriminator: hashtag of the user (ex. #1234)
|
||||
user: :name
|
||||
|
||||
# A user's guild-specific displayname - if they've set a custom nick in
|
||||
# a guild
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: username of the user
|
||||
# discriminator: hashtag of the user (ex. #1234)
|
||||
# displayname: the user's custom group-specific nick
|
||||
# channel: the name of the channel
|
||||
# guild: the name of the guild
|
||||
userOverride: :name
|
||||
|
||||
# Room names for bridged Discord channels
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: name of the channel
|
||||
# guild: name of the guild
|
||||
room: :name
|
||||
|
||||
# Group names for bridged Discord servers
|
||||
#
|
||||
# Available variables:
|
||||
#
|
||||
# name: name of the guide
|
||||
group: :name
|
||||
|
||||
database:
|
||||
# Use Postgres as a database backend. If set, will be used instead of SQLite3
|
||||
#
|
||||
# Connection string to connect to the Postgres instance
|
||||
# with username "user", password "pass", host "localhost" and database name "dbname".
|
||||
#
|
||||
# Modify each value as necessary
|
||||
#connString: "postgres://user:pass@localhost/dbname?sslmode=disable"
|
||||
|
||||
# Use SQLite3 as a database backend
|
||||
#
|
||||
# The name of the database file
|
||||
filename: database.db
|
||||
|
||||
limits:
|
||||
# Up to how many users should be auto-joined on room creation? -1 to disable
|
||||
# auto-join functionality
|
||||
#
|
||||
# Defaults to 200
|
||||
#maxAutojoinUsers: 200
|
||||
|
||||
# How long the delay between two auto-join users should be in milliseconds
|
||||
#
|
||||
# Defaults to 5000
|
||||
#roomUserAutojoinDelay: 5000
|
||||
|
||||
logging:
|
||||
# Log level of console output
|
||||
#
|
||||
# Allowed values starting with most verbose:
|
||||
# silly, verbose, info, warn, error
|
||||
console: info
|
||||
|
||||
# Date and time formatting
|
||||
lineDateFormat: MMM-D HH:mm:ss.SSS
|
||||
|
||||
# Logging files
|
||||
#
|
||||
# Log files are rotated daily by default
|
||||
files:
|
||||
# Log file path
|
||||
- file: "bridge.log"
|
||||
# Log level for this file
|
||||
#
|
||||
# Allowed values starting with most verbose:
|
||||
# silly, debug, verbose, info, warn, error
|
||||
level: info
|
||||
|
||||
# Date and time formatting
|
||||
datePattern: YYYY-MM-DD
|
||||
|
||||
# Maximum number of logs to keep.
|
||||
#
|
||||
# This can be a number of files or number of days.
|
||||
# If using days, add 'd' as a suffix
|
||||
maxFiles: 14d
|
||||
|
||||
# Maximum size of the file after which it will rotate.
|
||||
# This can be a number of bytes, or units of kb, mb, and gb.
|
||||
|
||||
# If using units, add 'k', 'm', or 'g' as the suffix
|
||||
maxSize: 50m
|
||||
337
matrix-discord/mx-puppet-discord/src/Commands.ts
Executable file
337
matrix-discord/mx-puppet-discord/src/Commands.ts
Executable file
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { App } from "./app";
|
||||
import { SendMessageFn, Log } from "mx-puppet-bridge";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { BridgeableGuildChannel } from "./discord/DiscordUtil";
|
||||
|
||||
const log = new Log("DiscordPuppet:Commands");
|
||||
const MAX_MSG_SIZE = 4000;
|
||||
|
||||
export class Commands {
|
||||
constructor(private readonly app: App) {}
|
||||
|
||||
public async commandSyncProfile(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const syncProfile = param === "1" || param.toLowerCase() === "true";
|
||||
p.data.syncProfile = syncProfile;
|
||||
await this.app.puppet.setPuppetData(puppetId, p.data);
|
||||
if (syncProfile) {
|
||||
await sendMessage("Syncing discord profile with matrix profile now");
|
||||
await this.app.updateUserInfo(puppetId);
|
||||
} else {
|
||||
await sendMessage("Stopped syncing discord profile with matrix profile");
|
||||
}
|
||||
}
|
||||
|
||||
public async commandJoinEntireGuild(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const guild = p.client.guilds.cache.get(param);
|
||||
if (!guild) {
|
||||
await sendMessage("Guild not found!");
|
||||
return;
|
||||
}
|
||||
if (!(await this.app.store.isGuildBridged(puppetId, guild.id))) {
|
||||
await sendMessage("Guild not bridged!");
|
||||
return;
|
||||
}
|
||||
for (const chan of guild.channels.cache.array()) {
|
||||
if (!this.app.discord.isBridgeableGuildChannel(chan)) {
|
||||
continue;
|
||||
}
|
||||
const gchan = chan as BridgeableGuildChannel;
|
||||
if (gchan.members.has(p.client.user!.id)) {
|
||||
const remoteChan = this.app.matrix.getRemoteRoom(puppetId, gchan);
|
||||
await this.app.puppet.bridgeRoom(remoteChan);
|
||||
}
|
||||
}
|
||||
await sendMessage(`Invited to all channels in guild ${guild.name}!`);
|
||||
}
|
||||
|
||||
public async commandListGuilds(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const guilds = await this.app.store.getBridgedGuilds(puppetId);
|
||||
let sendStr = "Guilds:\n";
|
||||
for (const guild of p.client.guilds.cache.array()) {
|
||||
let sendStrPart = ` - ${guild.name} (\`${guild.id}\`)`;
|
||||
if (guilds.includes(guild.id)) {
|
||||
sendStrPart += " **bridged!**";
|
||||
}
|
||||
sendStrPart += "\n";
|
||||
if (sendStr.length + sendStrPart.length > MAX_MSG_SIZE) {
|
||||
await sendMessage(sendStr);
|
||||
sendStr = "";
|
||||
}
|
||||
sendStr += sendStrPart;
|
||||
}
|
||||
await sendMessage(sendStr);
|
||||
}
|
||||
|
||||
public async commandAcceptInvite(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const matches = param.match(/^(?:https?:\/\/)?(?:discord\.gg\/|discordapp\.com\/invite\/)?([^?\/\s]+)/i);
|
||||
if (!matches) {
|
||||
await sendMessage("No invite code found!");
|
||||
return;
|
||||
}
|
||||
const inviteCode = matches[1];
|
||||
try {
|
||||
const guild = await p.client.acceptInvite(inviteCode);
|
||||
if (!guild) {
|
||||
await sendMessage("Something went wrong");
|
||||
} else {
|
||||
await sendMessage(`Accepted invite to guild ${guild.name}!`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.message) {
|
||||
await sendMessage(`Invalid invite code \`${inviteCode}\`: ${err.message}`);
|
||||
} else {
|
||||
await sendMessage(`Invalid invite code \`${inviteCode}\``);
|
||||
}
|
||||
log.warn(`Invalid invite code ${inviteCode}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
public async commandBridgeGuild(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const guild = p.client.guilds.cache.get(param);
|
||||
if (!guild) {
|
||||
await sendMessage("Guild not found!");
|
||||
return;
|
||||
}
|
||||
await this.app.store.setBridgedGuild(puppetId, guild.id);
|
||||
let msg = `Guild ${guild.name} (\`${guild.id}\`) is now being bridged!
|
||||
|
||||
Either type \`joinentireguild ${puppetId} ${guild.id}\` to get invited to all the channels of that guild `;
|
||||
msg += `or type \`listrooms\` and join that way.
|
||||
|
||||
Additionally you will be invited to guild channels as messages are sent in them.`;
|
||||
await sendMessage(msg);
|
||||
}
|
||||
|
||||
public async commandUnbridgeGuild(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const bridged = await this.app.store.isGuildBridged(puppetId, param);
|
||||
if (!bridged) {
|
||||
await sendMessage("Guild wasn't bridged!");
|
||||
return;
|
||||
}
|
||||
await this.app.store.removeBridgedGuild(puppetId, param);
|
||||
await sendMessage("Unbridged guild!");
|
||||
}
|
||||
|
||||
public async commandBridgeChannel(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
let channel: BridgeableGuildChannel | undefined;
|
||||
let guild: Discord.Guild | undefined;
|
||||
for (const g of p.client.guilds.cache.array()) {
|
||||
channel = g.channels.resolve(param) as BridgeableGuildChannel;
|
||||
if (this.app.discord.isBridgeableGuildChannel(channel)) {
|
||||
guild = g;
|
||||
break;
|
||||
}
|
||||
channel = undefined;
|
||||
}
|
||||
if (!channel || !guild) {
|
||||
await sendMessage("Channel not found!");
|
||||
return;
|
||||
}
|
||||
await this.app.store.setBridgedChannel(puppetId, channel.id);
|
||||
await sendMessage(`Channel ${channel.name} (\`${channel.id}\`) of guild ${guild.name} is now been bridged!`);
|
||||
}
|
||||
|
||||
public async commandUnbridgeChannel(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const bridged = await this.app.store.isChannelBridged(puppetId, param);
|
||||
if (!bridged) {
|
||||
await sendMessage("Channel wasn't bridged!");
|
||||
return;
|
||||
}
|
||||
await this.app.store.removeBridgedChannel(puppetId, param);
|
||||
await sendMessage("Unbridged channel!");
|
||||
}
|
||||
|
||||
public async commandBridgeAll(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
const bridgeAll = param === "1" || param.toLowerCase() === "true";
|
||||
p.data.bridgeAll = bridgeAll;
|
||||
await this.app.puppet.setPuppetData(puppetId, p.data);
|
||||
if (bridgeAll) {
|
||||
await sendMessage("Bridging everything now");
|
||||
} else {
|
||||
await sendMessage("Not bridging everything anymore");
|
||||
}
|
||||
}
|
||||
|
||||
public async commandEnableFriendsManagement(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
if (p.data.friendsManagement) {
|
||||
await sendMessage("Friends management is already enabled.");
|
||||
return;
|
||||
}
|
||||
if (param === "YES I KNOW THE RISKS") {
|
||||
p.data.friendsManagement = true;
|
||||
await this.app.puppet.setPuppetData(puppetId, p.data);
|
||||
await sendMessage("Friends management enabled!");
|
||||
return;
|
||||
}
|
||||
await sendMessage(`Using user accounts is against discords TOS. As this is required for friends management, you ` +
|
||||
`will be breaking discords TOS if you enable this feature. Development of it has already softlocked accounts. ` +
|
||||
`USE AT YOUR OWN RISK!\n\nIf you want to enable friends management type \`enablefriendsmanagement ${puppetId} ` +
|
||||
`YES I KNOW THE RISKS\``);
|
||||
}
|
||||
|
||||
public async commandListFriends(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
if (!p.data.friendsManagement) {
|
||||
await sendMessage(`Friends management is disabled. Please type ` +
|
||||
`\`enablefriendsmanagement ${puppetId}\` to enable it`);
|
||||
return;
|
||||
}
|
||||
let sendStr = "";
|
||||
const friends = p.client.user!.relationships.friends;
|
||||
if (friends.size > 0) {
|
||||
sendStr += "Friends:\n";
|
||||
for (const user of p.client.user!.relationships.friends.array()) {
|
||||
const mxid = await this.app.puppet.getMxidForUser({
|
||||
puppetId,
|
||||
userId: user.id,
|
||||
});
|
||||
const sendStrPart = ` - ${user.username} (\`${user.id}\`): [${user.username}](https://matrix.to/#/${mxid})\n`;
|
||||
if (sendStr.length + sendStrPart.length > MAX_MSG_SIZE) {
|
||||
await sendMessage(sendStr);
|
||||
sendStr = "";
|
||||
}
|
||||
sendStr += sendStrPart;
|
||||
}
|
||||
}
|
||||
const incoming = p.client.user!.relationships.incoming;
|
||||
if (incoming.size > 0) {
|
||||
sendStr += "\nIncoming friend requests:\n";
|
||||
for (const user of incoming.array()) {
|
||||
const sendStrPart = ` - ${user.username} (\`${user.id}\`)\n`;
|
||||
if (sendStr.length + sendStrPart.length > MAX_MSG_SIZE) {
|
||||
await sendMessage(sendStr);
|
||||
sendStr = "";
|
||||
}
|
||||
sendStr += sendStrPart;
|
||||
}
|
||||
}
|
||||
const outgoing = p.client.user!.relationships.outgoing;
|
||||
if (outgoing.size > 0) {
|
||||
sendStr += "\nOutgoing friend requests:\n";
|
||||
for (const user of outgoing.array()) {
|
||||
const sendStrPart = ` - ${user.username} (\`${user.id}\`)\n`;
|
||||
if (sendStr.length + sendStrPart.length > MAX_MSG_SIZE) {
|
||||
await sendMessage(sendStr);
|
||||
sendStr = "";
|
||||
}
|
||||
sendStr += sendStrPart;
|
||||
}
|
||||
}
|
||||
await sendMessage(sendStr);
|
||||
}
|
||||
|
||||
public async commandAddFriend(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
if (!p.data.friendsManagement) {
|
||||
await sendMessage(`Friends management is disabled. Please type ` +
|
||||
`\`enablefriendsmanagement ${puppetId}\` to enable it`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await p.client.user!.relationships.request("friend", param);
|
||||
if (user) {
|
||||
await sendMessage(`Added/sent friend request to ${typeof user === "string" ? user : user.username}!`);
|
||||
} else {
|
||||
await sendMessage("User not found");
|
||||
}
|
||||
} catch (err) {
|
||||
await sendMessage("User not found");
|
||||
log.warn(`Couldn't find user ${param}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
public async commandRemoveFriend(puppetId: number, param: string, sendMessage: SendMessageFn) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
await sendMessage("Puppet not found!");
|
||||
return;
|
||||
}
|
||||
if (!p.data.friendsManagement) {
|
||||
await sendMessage(`Friends management is disabled. Please type ` +
|
||||
`\`enablefriendsmanagement ${puppetId}\` to enable it`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await p.client.user!.relationships.remove(param);
|
||||
if (user) {
|
||||
await sendMessage(`Removed ${user.username} as friend!`);
|
||||
} else {
|
||||
await sendMessage("User not found");
|
||||
}
|
||||
} catch (err) {
|
||||
await sendMessage("User not found");
|
||||
log.warn(`Couldn't find user ${param}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
477
matrix-discord/mx-puppet-discord/src/app.ts
Executable file
477
matrix-discord/mx-puppet-discord/src/app.ts
Executable file
@@ -0,0 +1,477 @@
|
||||
/* tslint:disable: no-any */
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
PuppetBridge,
|
||||
Log,
|
||||
Util,
|
||||
IRetList,
|
||||
MessageDeduplicator,
|
||||
IRemoteRoom,
|
||||
} from "mx-puppet-bridge";
|
||||
import * as Discord from "better-discord.js";
|
||||
import {
|
||||
DiscordMessageParser,
|
||||
MatrixMessageParser,
|
||||
} from "matrix-discord-parser";
|
||||
import * as path from "path";
|
||||
import * as mime from "mime";
|
||||
import { DiscordStore } from "./store";
|
||||
import {
|
||||
DiscordUtil, TextGuildChannel, TextChannel, BridgeableGuildChannel, BridgeableChannel,
|
||||
} from "./discord/DiscordUtil";
|
||||
import { MatrixUtil } from "./matrix/MatrixUtil";
|
||||
import { Commands } from "./Commands";
|
||||
import ExpireSet from "expire-set";
|
||||
|
||||
const log = new Log("DiscordPuppet:App");
|
||||
export const AVATAR_SETTINGS: Discord.ImageURLOptions & { dynamic?: boolean | undefined; }
|
||||
= { format: "png", size: 2048, dynamic: true };
|
||||
export const MAXFILESIZE = 8000000;
|
||||
|
||||
export interface IDiscordPuppet {
|
||||
client: Discord.Client;
|
||||
data: any;
|
||||
deletedMessages: ExpireSet<string>;
|
||||
}
|
||||
|
||||
export interface IDiscordPuppets {
|
||||
[puppetId: number]: IDiscordPuppet;
|
||||
}
|
||||
|
||||
export interface IDiscordSendFile {
|
||||
buffer: Buffer;
|
||||
filename: string;
|
||||
url: string;
|
||||
isImage: boolean;
|
||||
}
|
||||
|
||||
export class App {
|
||||
public puppets: IDiscordPuppets = {};
|
||||
public discordMsgParser: DiscordMessageParser;
|
||||
public matrixMsgParser: MatrixMessageParser;
|
||||
public messageDeduplicator: MessageDeduplicator;
|
||||
public store: DiscordStore;
|
||||
public lastEventIds: {[chan: string]: string} = {};
|
||||
|
||||
public readonly discord: DiscordUtil;
|
||||
public readonly matrix: MatrixUtil;
|
||||
public readonly commands: Commands;
|
||||
|
||||
constructor(
|
||||
public puppet: PuppetBridge,
|
||||
) {
|
||||
this.discordMsgParser = new DiscordMessageParser();
|
||||
this.matrixMsgParser = new MatrixMessageParser();
|
||||
this.messageDeduplicator = new MessageDeduplicator();
|
||||
this.store = new DiscordStore(puppet.store);
|
||||
|
||||
this.discord = new DiscordUtil(this);
|
||||
this.matrix = new MatrixUtil(this);
|
||||
this.commands = new Commands(this);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.store.init();
|
||||
}
|
||||
|
||||
public async handlePuppetName(puppetId: number, name: string) {
|
||||
const p = this.puppets[puppetId];
|
||||
if (!p || !p.data.syncProfile || !p.client.user!.bot) {
|
||||
// bots can't change their name
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await p.client.user!.setUsername(name);
|
||||
} catch (err) {
|
||||
log.warn(`Couldn't set name for ${puppetId}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
public async handlePuppetAvatar(puppetId: number, url: string, mxc: string) {
|
||||
const p = this.puppets[puppetId];
|
||||
if (!p || !p.data.syncProfile) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const AVATAR_SIZE = 800;
|
||||
const realUrl = this.puppet.getUrlFromMxc(mxc, AVATAR_SIZE, AVATAR_SIZE, "scale");
|
||||
const buffer = await Util.DownloadFile(realUrl);
|
||||
await p.client.user!.setAvatar(buffer);
|
||||
} catch (err) {
|
||||
log.warn(`Couldn't set avatar for ${puppetId}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
public async newPuppet(puppetId: number, data: any) {
|
||||
log.info(`Adding new Puppet: puppetId=${puppetId}`);
|
||||
if (this.puppets[puppetId]) {
|
||||
await this.deletePuppet(puppetId);
|
||||
}
|
||||
const client = new Discord.Client();
|
||||
client.on("ready", async () => {
|
||||
const d = this.puppets[puppetId].data;
|
||||
d.username = client.user!.tag;
|
||||
d.id = client.user!.id;
|
||||
d.bot = client.user!.bot;
|
||||
await this.puppet.setUserId(puppetId, client.user!.id);
|
||||
await this.puppet.setPuppetData(puppetId, d);
|
||||
await this.puppet.sendStatusMessage(puppetId, "connected");
|
||||
await this.updateUserInfo(puppetId);
|
||||
// set initial presence for everyone
|
||||
for (const user of client.users.cache.array()) {
|
||||
await this.discord.updatePresence(puppetId, user.presence);
|
||||
}
|
||||
});
|
||||
client.on("message", async (msg: Discord.Message) => {
|
||||
try {
|
||||
await this.discord.events.handleDiscordMessage(puppetId, msg);
|
||||
} catch (err) {
|
||||
log.error("Error handling discord message event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("messageUpdate", async (msg1: Discord.Message, msg2: Discord.Message) => {
|
||||
try {
|
||||
await this.discord.events.handleDiscordMessageUpdate(puppetId, msg1, msg2);
|
||||
} catch (err) {
|
||||
log.error("Error handling discord messageUpdate event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("messageDelete", async (msg: Discord.Message) => {
|
||||
try {
|
||||
await this.discord.events.handleDiscordMessageDelete(puppetId, msg);
|
||||
} catch (err) {
|
||||
log.error("Error handling discord messageDelete event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("messageDeleteBulk", async (msgs: Discord.Collection<Discord.Snowflake, Discord.Message>) => {
|
||||
for (const msg of msgs.array()) {
|
||||
try {
|
||||
await this.discord.events.handleDiscordMessageDelete(puppetId, msg);
|
||||
} catch (err) {
|
||||
log.error("Error handling one discord messageDeleteBulk event", err.error || err.body || err);
|
||||
}
|
||||
}
|
||||
});
|
||||
client.on("typingStart", async (chan: Discord.Channel, user: Discord.User) => {
|
||||
try {
|
||||
if (!this.discord.isBridgeableChannel(chan)) {
|
||||
return;
|
||||
}
|
||||
const params = this.matrix.getSendParams(puppetId, chan as BridgeableChannel, user);
|
||||
await this.puppet.setUserTyping(params, true);
|
||||
} catch (err) {
|
||||
log.error("Error handling discord typingStart event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("presenceUpdate", async (_, presence: Discord.Presence) => {
|
||||
try {
|
||||
await this.discord.updatePresence(puppetId, presence);
|
||||
} catch (err) {
|
||||
log.error("Error handling discord presenceUpdate event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("messageReactionAdd", async (reaction: Discord.MessageReaction, user: Discord.User) => {
|
||||
try {
|
||||
// TODO: filter out echo back?
|
||||
const chan = reaction.message.channel;
|
||||
if (!await this.bridgeRoom(puppetId, chan)) {
|
||||
return;
|
||||
}
|
||||
const params = this.matrix.getSendParams(puppetId, chan, user);
|
||||
if (reaction.emoji.id) {
|
||||
const mxc = await this.matrix.getEmojiMxc(
|
||||
puppetId, reaction.emoji.name, reaction.emoji.animated, reaction.emoji.id,
|
||||
);
|
||||
await this.puppet.sendReaction(params, reaction.message.id, mxc || reaction.emoji.name);
|
||||
} else {
|
||||
await this.puppet.sendReaction(params, reaction.message.id, reaction.emoji.name);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("Error handling discord messageReactionAdd event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("messageReactionRemove", async (reaction: Discord.MessageReaction, user: Discord.User) => {
|
||||
try {
|
||||
// TODO: filter out echo back?
|
||||
const chan = reaction.message.channel;
|
||||
if (!await this.bridgeRoom(puppetId, chan)) {
|
||||
return;
|
||||
}
|
||||
const params = this.matrix.getSendParams(puppetId, chan, user);
|
||||
if (reaction.emoji.id) {
|
||||
const mxc = await this.matrix.getEmojiMxc(
|
||||
puppetId, reaction.emoji.name, reaction.emoji.animated, reaction.emoji.id,
|
||||
);
|
||||
await this.puppet.removeReaction(params, reaction.message.id, mxc || reaction.emoji.name);
|
||||
} else {
|
||||
await this.puppet.removeReaction(params, reaction.message.id, reaction.emoji.name);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("Error handling discord messageReactionRemove event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("messageReactionRemoveAll", async (message: Discord.Message) => {
|
||||
try {
|
||||
const chan = message.channel;
|
||||
if (!await this.bridgeRoom(puppetId, chan)) {
|
||||
return;
|
||||
}
|
||||
// alright, let's fetch *an* admin user
|
||||
let user: Discord.User;
|
||||
if (this.discord.isBridgeableGuildChannel(chan)) {
|
||||
const gchan = chan as BridgeableGuildChannel;
|
||||
user = gchan.guild.owner ? gchan.guild.owner.user : client.user!;
|
||||
} else if (chan instanceof Discord.DMChannel) {
|
||||
user = chan.recipient;
|
||||
} else if (chan instanceof Discord.GroupDMChannel) {
|
||||
user = chan.owner;
|
||||
} else {
|
||||
user = client.user!;
|
||||
}
|
||||
const params = this.matrix.getSendParams(puppetId, chan, user);
|
||||
await this.puppet.removeAllReactions(params, message.id);
|
||||
} catch (err) {
|
||||
log.error("Error handling discord messageReactionRemoveAll event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("channelUpdate", async (_, chan: Discord.Channel) => {
|
||||
if (!this.discord.isBridgeableChannel(chan)) {
|
||||
return;
|
||||
}
|
||||
const remoteChan = this.matrix.getRemoteRoom(puppetId, chan as BridgeableChannel);
|
||||
await this.puppet.updateRoom(remoteChan);
|
||||
});
|
||||
client.on("guildMemberUpdate", async (oldMember: Discord.GuildMember, newMember: Discord.GuildMember) => {
|
||||
const promiseList: Promise<void>[] = [];
|
||||
if (oldMember.displayName !== newMember.displayName) {
|
||||
promiseList.push((async () => {
|
||||
const remoteUser = this.matrix.getRemoteUser(puppetId, newMember);
|
||||
await this.puppet.updateUser(remoteUser);
|
||||
})());
|
||||
}
|
||||
// aaaand check for role change
|
||||
const leaveRooms = new Set<BridgeableGuildChannel>();
|
||||
const joinRooms = new Set<BridgeableGuildChannel>();
|
||||
for (const chan of newMember.guild.channels.cache.array()) {
|
||||
if (!this.discord.isBridgeableGuildChannel(chan)) {
|
||||
continue;
|
||||
}
|
||||
const gchan = chan as BridgeableGuildChannel;
|
||||
if (gchan.members.has(newMember.id)) {
|
||||
joinRooms.add(gchan);
|
||||
} else {
|
||||
leaveRooms.add(gchan);
|
||||
}
|
||||
}
|
||||
for (const chan of leaveRooms) {
|
||||
promiseList.push((async () => {
|
||||
const params = this.matrix.getSendParams(puppetId, chan, newMember);
|
||||
await this.puppet.removeUser(params);
|
||||
})());
|
||||
}
|
||||
for (const chan of joinRooms) {
|
||||
promiseList.push((async () => {
|
||||
const params = this.matrix.getSendParams(puppetId, chan, newMember);
|
||||
await this.puppet.addUser(params);
|
||||
})());
|
||||
}
|
||||
await Promise.all(promiseList);
|
||||
});
|
||||
client.on("userUpdate", async (_, user: Discord.User) => {
|
||||
const remoteUser = this.matrix.getRemoteUser(puppetId, user);
|
||||
await this.puppet.updateUser(remoteUser);
|
||||
});
|
||||
client.on("guildUpdate", async (_, guild: Discord.Guild) => {
|
||||
try {
|
||||
const remoteGroup = await this.matrix.getRemoteGroup(puppetId, guild);
|
||||
await this.puppet.updateGroup(remoteGroup);
|
||||
for (const chan of guild.channels.cache.array()) {
|
||||
if (!this.discord.isBridgeableGuildChannel(chan)) {
|
||||
return;
|
||||
}
|
||||
const remoteChan = this.matrix.getRemoteRoom(puppetId, chan as BridgeableGuildChannel);
|
||||
await this.puppet.updateRoom(remoteChan);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("Error handling discord guildUpdate event", err.error || err.body || err);
|
||||
}
|
||||
});
|
||||
client.on("relationshipAdd", async (_, relationship: Discord.Relationship) => {
|
||||
if (relationship.type === "incoming") {
|
||||
const msg = `New incoming friends request from ${relationship.user.username}!
|
||||
|
||||
Type \`addfriend ${puppetId} ${relationship.user.id}\` to accept it.`;
|
||||
await this.puppet.sendStatusMessage(puppetId, msg);
|
||||
}
|
||||
});
|
||||
client.on("guildMemberAdd", async (member: Discord.GuildMember) => {
|
||||
const promiseList: Promise<void>[] = [];
|
||||
for (const chan of member.guild.channels.cache.array()) {
|
||||
if ((await this.bridgeRoom(puppetId, chan)) && chan.members.has(member.id)) {
|
||||
promiseList.push((async () => {
|
||||
const params = this.matrix.getSendParams(puppetId, chan as BridgeableGuildChannel, member);
|
||||
await this.puppet.addUser(params);
|
||||
})());
|
||||
}
|
||||
}
|
||||
await Promise.all(promiseList);
|
||||
});
|
||||
client.on("guildMemberRemove", async (member: Discord.GuildMember) => {
|
||||
const promiseList: Promise<void>[] = [];
|
||||
for (const chan of member.guild.channels.cache.array()) {
|
||||
if (this.discord.isBridgeableGuildChannel(chan)) {
|
||||
promiseList.push((async () => {
|
||||
const params = this.matrix.getSendParams(puppetId, chan as BridgeableGuildChannel, member);
|
||||
await this.puppet.removeUser(params);
|
||||
})());
|
||||
}
|
||||
}
|
||||
await Promise.all(promiseList);
|
||||
});
|
||||
const TWO_MIN = 120000;
|
||||
this.puppets[puppetId] = {
|
||||
client,
|
||||
data,
|
||||
deletedMessages: new ExpireSet(TWO_MIN),
|
||||
};
|
||||
await client.login(data.token, data.bot || false);
|
||||
}
|
||||
|
||||
public async deletePuppet(puppetId: number) {
|
||||
log.info(`Got signal to quit Puppet: puppetId=${puppetId}`);
|
||||
const p = this.puppets[puppetId];
|
||||
if (!p) {
|
||||
return; // nothing to do
|
||||
}
|
||||
p.client.destroy();
|
||||
delete this.puppet[puppetId];
|
||||
}
|
||||
|
||||
public async listUsers(puppetId: number): Promise<IRetList[]> {
|
||||
const retUsers: IRetList[] = [];
|
||||
const retGuilds: IRetList[] = [];
|
||||
const p = this.puppets[puppetId];
|
||||
if (!p) {
|
||||
return [];
|
||||
}
|
||||
const blacklistedIds = [p.client.user!.id, "1"];
|
||||
for (const guild of p.client.guilds.cache.array()) {
|
||||
retGuilds.push({
|
||||
category: true,
|
||||
name: guild.name,
|
||||
});
|
||||
for (const member of guild.members.cache.array()) {
|
||||
if (!blacklistedIds.includes(member.user.id)) {
|
||||
retGuilds.push({
|
||||
name: member.user.username,
|
||||
id: member.user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const user of p.client.users.cache.array()) {
|
||||
const found = retGuilds.find((element) => element.id === user.id);
|
||||
if (!found && !blacklistedIds.includes(user.id)) {
|
||||
retUsers.push({
|
||||
name: user.username,
|
||||
id: user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return retUsers.concat(retGuilds);
|
||||
}
|
||||
|
||||
public async getUserIdsInRoom(room: IRemoteRoom): Promise<Set<string> | null> {
|
||||
const chan = await this.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
return null;
|
||||
}
|
||||
const users = new Set<string>();
|
||||
if (chan instanceof Discord.DMChannel) {
|
||||
users.add(chan.recipient.id);
|
||||
return users;
|
||||
}
|
||||
if (chan instanceof Discord.GroupDMChannel) {
|
||||
for (const recipient of chan.recipients.array()) {
|
||||
users.add(recipient.id);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
if (this.discord.isBridgeableGuildChannel(chan)) {
|
||||
// chan.members already does a permission check, yay!
|
||||
const gchan = chan as BridgeableGuildChannel;
|
||||
for (const member of gchan.members.array()) {
|
||||
users.add(member.id);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async updateUserInfo(puppetId: number) {
|
||||
const p = this.puppets[puppetId];
|
||||
if (!p || !p.data.syncProfile) {
|
||||
return;
|
||||
}
|
||||
const userInfo = await this.puppet.getPuppetMxidInfo(puppetId);
|
||||
if (userInfo) {
|
||||
if (userInfo.name) {
|
||||
await this.handlePuppetName(puppetId, userInfo.name);
|
||||
}
|
||||
if (userInfo.avatarUrl) {
|
||||
await this.handlePuppetAvatar(puppetId, userInfo.avatarUrl, userInfo.avatarMxc as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async bridgeRoom(puppetId: number, chan: Discord.Channel): Promise<boolean> {
|
||||
if (["dm", "group"].includes(chan.type)) {
|
||||
return true; // we handle all dm and group channels
|
||||
}
|
||||
if (!this.discord.isBridgeableChannel(chan)) {
|
||||
return false; // we only handle text and news things
|
||||
}
|
||||
if (this.puppets[puppetId] && this.puppets[puppetId].data.bridgeAll) {
|
||||
return true; // we want to bridge everything anyways, no need to hit the store
|
||||
}
|
||||
if (this.discord.isBridgeableGuildChannel(chan)) {
|
||||
// we have a guild text channel, maybe we handle it!
|
||||
const gchan = chan as BridgeableGuildChannel;
|
||||
if (await this.store.isGuildBridged(puppetId, gchan.guild.id)) {
|
||||
return true;
|
||||
}
|
||||
// maybe it is a single channel override?
|
||||
return await this.store.isChannelBridged(puppetId, gchan.id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getFilenameForMedia(filename: string, mimetype: string): string {
|
||||
let ext = "";
|
||||
const mimeExt = mime.getExtension(mimetype);
|
||||
if (mimeExt) {
|
||||
ext = "." + mimeExt;
|
||||
}
|
||||
if (filename) {
|
||||
if (path.extname(filename) !== "") {
|
||||
return filename;
|
||||
}
|
||||
return path.basename(filename) + ext;
|
||||
}
|
||||
return "matrix-media" + ext;
|
||||
}
|
||||
}
|
||||
37
matrix-discord/mx-puppet-discord/src/db/schema/v1.ts
Executable file
37
matrix-discord/mx-puppet-discord/src/db/schema/v1.ts
Executable file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2019 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDbSchema, Store } from "mx-puppet-bridge";
|
||||
|
||||
export class Schema implements IDbSchema {
|
||||
public description = "Schema, Emotestore";
|
||||
public async run(store: Store) {
|
||||
await store.createTable(`
|
||||
CREATE TABLE discord_schema (
|
||||
version INTEGER UNIQUE NOT NULL
|
||||
);`, "discord_schema");
|
||||
await store.db.Exec("INSERT INTO discord_schema VALUES (0);");
|
||||
await store.createTable(`
|
||||
CREATE TABLE discord_emoji (
|
||||
emoji_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
animated INTEGER NOT NULL,
|
||||
mxc_url TEXT NOT NULL,
|
||||
PRIMARY KEY(emoji_id)
|
||||
);`, "discord_emoji");
|
||||
}
|
||||
public async rollBack(store: Store) {
|
||||
await store.db.Exec("DROP TABLE IF EXISTS discord_schema");
|
||||
await store.db.Exec("DROP TABLE IF EXISTS discord_emoji");
|
||||
}
|
||||
}
|
||||
29
matrix-discord/mx-puppet-discord/src/db/schema/v2.ts
Executable file
29
matrix-discord/mx-puppet-discord/src/db/schema/v2.ts
Executable file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDbSchema, Store } from "mx-puppet-bridge";
|
||||
|
||||
export class Schema implements IDbSchema {
|
||||
public description = "Guilds Bridged";
|
||||
public async run(store: Store) {
|
||||
await store.createTable(`
|
||||
CREATE TABLE discord_bridged_guilds (
|
||||
id SERIAL PRIMARY KEY,
|
||||
puppet_id INTEGER NOT NULL,
|
||||
guild_id TEXT NOT NULL
|
||||
);`, "discord_bridged_guilds");
|
||||
}
|
||||
public async rollBack(store: Store) {
|
||||
await store.db.Exec("DROP TABLE IF EXISTS discord_bridged_guilds");
|
||||
}
|
||||
}
|
||||
29
matrix-discord/mx-puppet-discord/src/db/schema/v3.ts
Executable file
29
matrix-discord/mx-puppet-discord/src/db/schema/v3.ts
Executable file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDbSchema, Store } from "mx-puppet-bridge";
|
||||
|
||||
export class Schema implements IDbSchema {
|
||||
public description = "Channels Bridged";
|
||||
public async run(store: Store) {
|
||||
await store.createTable(`
|
||||
CREATE TABLE discord_bridged_channels (
|
||||
id SERIAL PRIMARY KEY,
|
||||
puppet_id INTEGER NOT NULL,
|
||||
channel_id TEXT NOT NULL
|
||||
);`, "discord_bridged_channels");
|
||||
}
|
||||
public async rollBack(store: Store) {
|
||||
await store.db.Exec("DROP TABLE IF EXISTS discord_bridged_channels");
|
||||
}
|
||||
}
|
||||
59
matrix-discord/mx-puppet-discord/src/db/schema/v4.ts
Executable file
59
matrix-discord/mx-puppet-discord/src/db/schema/v4.ts
Executable file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Log, IDbSchema, Store } from "mx-puppet-bridge";
|
||||
|
||||
export class Schema implements IDbSchema {
|
||||
public description = "migrate dm room IDs";
|
||||
public async run(store: Store) {
|
||||
try {
|
||||
let rows: any[];
|
||||
try {
|
||||
rows = await store.db.All("SELECT * FROM chan_store WHERE room_id LIKE 'dm%'");
|
||||
} catch (e) {
|
||||
rows = await store.db.All("SELECT * FROM room_store WHERE room_id LIKE 'dm%'");
|
||||
}
|
||||
for (const row of rows) {
|
||||
const parts = (row.room_id as string).split("-");
|
||||
row.room_id = `dm-${row.puppet_id}-${parts[1]}`;
|
||||
try {
|
||||
await store.db.Run(`UPDATE chan_store SET
|
||||
room_id = $room_id,
|
||||
puppet_id = $puppet_id,
|
||||
name = $name,
|
||||
avatar_url = $avatar_url,
|
||||
avatar_mxc = $avatar_mxc,
|
||||
avatar_hash = $avatar_hash,
|
||||
topic = $topic,
|
||||
group_id = $group_id
|
||||
WHERE mxid = $mxid`, row);
|
||||
} catch (e) {
|
||||
await store.db.Run(`UPDATE room_store SET
|
||||
room_id = $room_id,
|
||||
puppet_id = $puppet_id,
|
||||
name = $name,
|
||||
avatar_url = $avatar_url,
|
||||
avatar_mxc = $avatar_mxc,
|
||||
avatar_hash = $avatar_hash,
|
||||
topic = $topic,
|
||||
group_id = $group_id
|
||||
WHERE mxid = $mxid`, row);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const log = new Log("DiscordPuppet::DbUpgrade");
|
||||
log.error("Failed to migrate room ID data:", err);
|
||||
}
|
||||
}
|
||||
public async rollBack(store: Store) { } // no rollback
|
||||
}
|
||||
141
matrix-discord/mx-puppet-discord/src/discord/DiscordEventHandler.ts
Executable file
141
matrix-discord/mx-puppet-discord/src/discord/DiscordEventHandler.ts
Executable file
@@ -0,0 +1,141 @@
|
||||
/* tslint:disable: no-any */
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { App } from "../app";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { IDiscordMessageParserOpts, DiscordMessageParser } from "matrix-discord-parser";
|
||||
import { Log } from "mx-puppet-bridge";
|
||||
import { TextGuildChannel } from "./DiscordUtil";
|
||||
|
||||
const log = new Log("DiscordPuppet:DiscordEventHandler");
|
||||
|
||||
export class DiscordEventHandler {
|
||||
private discordMsgParser: DiscordMessageParser;
|
||||
|
||||
public constructor(private readonly app: App) {
|
||||
this.discordMsgParser = this.app.discordMsgParser;
|
||||
}
|
||||
|
||||
public async handleDiscordMessage(puppetId: number, msg: Discord.Message) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
if (msg.type !== "DEFAULT") {
|
||||
return;
|
||||
}
|
||||
log.info("Received new message!");
|
||||
if (!await this.app.bridgeRoom(puppetId, msg.channel)) {
|
||||
log.info("Unhandled channel, dropping message...");
|
||||
return;
|
||||
}
|
||||
const params = this.app.matrix.getSendParams(puppetId, msg);
|
||||
const lockKey = `${puppetId};${msg.channel.id}`;
|
||||
const dedupeMsg = msg.attachments.first() ? `file:${msg.attachments.first()!.name}` : msg.content;
|
||||
if (await this.app.messageDeduplicator.dedupe(lockKey, msg.author.id, msg.id, dedupeMsg)) {
|
||||
// dedupe message
|
||||
log.info("Deduping message, dropping...");
|
||||
return;
|
||||
}
|
||||
if (msg.webhookID && this.app.discord.isTextGuildChannel(msg.channel)) {
|
||||
// maybe we are a webhook from our webhook?
|
||||
const chan = msg.channel as TextGuildChannel;
|
||||
try {
|
||||
const hook = (await chan.fetchWebhooks()).find((h) => h.name === "_matrix") || null;
|
||||
if (hook && msg.webhookID === hook.id) {
|
||||
log.info("Message sent from our webhook, deduping...");
|
||||
return;
|
||||
}
|
||||
} catch (err) { } // no webhook permissions, ignore
|
||||
}
|
||||
this.app.lastEventIds[msg.channel.id] = msg.id;
|
||||
const externalUrl = params.externalUrl;
|
||||
for (const attachment of msg.attachments.array()) {
|
||||
params.externalUrl = attachment.url;
|
||||
await this.app.puppet.sendFileDetect(params, attachment.url, attachment.name);
|
||||
}
|
||||
params.externalUrl = externalUrl;
|
||||
if (msg.content || msg.embeds.length > 0) {
|
||||
const opts: IDiscordMessageParserOpts = {
|
||||
callbacks: this.app.discord.getDiscordMsgParserCallbacks(puppetId),
|
||||
};
|
||||
const reply = await this.discordMsgParser.FormatMessage(opts, msg);
|
||||
await this.app.puppet.sendMessage(params, {
|
||||
body: reply.body,
|
||||
formattedBody: reply.formattedBody,
|
||||
emote: reply.msgtype === "m.emote",
|
||||
notice: reply.msgtype === "m.notice",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async handleDiscordMessageUpdate(puppetId: number, msg1: Discord.Message, msg2: Discord.Message) {
|
||||
if (msg1.content === msg2.content) {
|
||||
return; // nothing to do
|
||||
}
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
const params = this.app.matrix.getSendParams(puppetId, msg1);
|
||||
const lockKey = `${puppetId};${msg1.channel.id}`;
|
||||
if (await this.app.messageDeduplicator.dedupe(lockKey, msg2.author.id, msg2.id, msg2.content)) {
|
||||
// dedupe message
|
||||
return;
|
||||
}
|
||||
if (!await this.app.bridgeRoom(puppetId, msg1.channel)) {
|
||||
log.info("Unhandled channel, dropping message...");
|
||||
return;
|
||||
}
|
||||
const opts: IDiscordMessageParserOpts = {
|
||||
callbacks: this.app.discord.getDiscordMsgParserCallbacks(puppetId),
|
||||
};
|
||||
const reply = await this.discordMsgParser.FormatMessage(opts, msg2);
|
||||
if (msg1.content) {
|
||||
// okay we have an actual edit
|
||||
await this.app.puppet.sendEdit(params, msg1.id, {
|
||||
body: reply.body,
|
||||
formattedBody: reply.formattedBody,
|
||||
emote: reply.msgtype === "m.emote",
|
||||
notice: reply.msgtype === "m.notice",
|
||||
});
|
||||
} else {
|
||||
// we actually just want to insert a new message
|
||||
await this.app.puppet.sendMessage(params, {
|
||||
body: reply.body,
|
||||
formattedBody: reply.formattedBody,
|
||||
emote: reply.msgtype === "m.emote",
|
||||
notice: reply.msgtype === "m.notice",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async handleDiscordMessageDelete(puppetId: number, msg: Discord.Message) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
const params = this.app.matrix.getSendParams(puppetId, msg);
|
||||
const lockKey = `${puppetId};${msg.channel.id}`;
|
||||
if (p.deletedMessages.has(msg.id) ||
|
||||
await this.app.messageDeduplicator.dedupe(lockKey, msg.author.id, msg.id, msg.content)) {
|
||||
// dedupe message
|
||||
return;
|
||||
}
|
||||
if (!await this.app.bridgeRoom(puppetId, msg.channel)) {
|
||||
log.info("Unhandled channel, dropping message...");
|
||||
return;
|
||||
}
|
||||
await this.app.puppet.sendRedact(params, msg.id);
|
||||
}
|
||||
}
|
||||
356
matrix-discord/mx-puppet-discord/src/discord/DiscordUtil.ts
Executable file
356
matrix-discord/mx-puppet-discord/src/discord/DiscordUtil.ts
Executable file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { App, IDiscordSendFile } from "../app";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { DiscordEventHandler } from "./DiscordEventHandler";
|
||||
import { ISendingUser, Log, IRemoteRoom } from "mx-puppet-bridge";
|
||||
import { IDiscordMessageParserCallbacks } from "matrix-discord-parser";
|
||||
|
||||
const log = new Log("DiscordPuppet:DiscordUtil");
|
||||
|
||||
export type TextGuildChannel = Discord.TextChannel | Discord.NewsChannel;
|
||||
export type TextChannel = TextGuildChannel | Discord.DMChannel | Discord.GroupDMChannel;
|
||||
|
||||
export type BridgeableGuildChannel = Discord.TextChannel | Discord.NewsChannel;
|
||||
export type BridgeableChannel = BridgeableGuildChannel | Discord.DMChannel | Discord.GroupDMChannel;
|
||||
|
||||
export class DiscordUtil {
|
||||
public readonly events: DiscordEventHandler;
|
||||
|
||||
public constructor(private readonly app: App) {
|
||||
this.events = new DiscordEventHandler(app);
|
||||
}
|
||||
|
||||
public async getDiscordChan(
|
||||
room: IRemoteRoom,
|
||||
): Promise<BridgeableChannel | null> {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
const id = room.roomId;
|
||||
const client = p.client;
|
||||
if (!id.startsWith("dm-")) {
|
||||
// first fetch from the client channel cache
|
||||
const chan = client.channels.resolve(id);
|
||||
if (this.isBridgeableChannel(chan)) {
|
||||
return chan as BridgeableChannel;
|
||||
}
|
||||
// next iterate over all the guild channels
|
||||
for (const guild of client.guilds.cache.array()) {
|
||||
const c = guild.channels.resolve(id);
|
||||
if (this.isBridgeableChannel(c)) {
|
||||
return c as BridgeableChannel;
|
||||
}
|
||||
}
|
||||
return null; // nothing found
|
||||
} else {
|
||||
// we have a DM channel
|
||||
const parts = id.split("-");
|
||||
if (Number(parts[1]) !== room.puppetId) {
|
||||
return null;
|
||||
}
|
||||
const PART_USERID = 2;
|
||||
const lookupId = parts[PART_USERID];
|
||||
const user = await this.getUserById(client, lookupId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const chan = await user.createDM();
|
||||
return chan;
|
||||
}
|
||||
}
|
||||
|
||||
public async sendToDiscord(
|
||||
chan: TextChannel,
|
||||
msg: string | IDiscordSendFile,
|
||||
asUser: ISendingUser | null,
|
||||
replyEmbed?: Discord.MessageEmbed,
|
||||
): Promise<Discord.Message | Discord.Message[]> {
|
||||
log.debug("Sending something to discord...");
|
||||
let sendThing: string | Discord.MessageAdditions;
|
||||
if (typeof msg === "string") {
|
||||
sendThing = msg;
|
||||
} else {
|
||||
sendThing = new Discord.MessageAttachment(msg.buffer, msg.filename);
|
||||
}
|
||||
if (!asUser) {
|
||||
// we don't want to relay, so just send off nicely
|
||||
log.debug("Not in relay mode, just sending as user");
|
||||
if (replyEmbed && chan.client.user!.bot) {
|
||||
return await chan.send(sendThing, replyEmbed);
|
||||
}
|
||||
return await chan.send(sendThing);
|
||||
}
|
||||
// alright, we have to send as if it was another user. First try webhooks.
|
||||
if (this.isTextGuildChannel(chan)) {
|
||||
chan = chan as TextGuildChannel;
|
||||
log.debug("Trying to send as webhook...");
|
||||
let hook: Discord.Webhook | null = null;
|
||||
try {
|
||||
hook = (await chan.fetchWebhooks()).find((h) => h.name === "_matrix") || null;
|
||||
if (!hook) {
|
||||
try {
|
||||
hook = await chan.createWebhook("_matrix", {
|
||||
reason: "Allow bridging matrix messages to discord nicely",
|
||||
});
|
||||
} catch (err) {
|
||||
log.warn("Unable to create \"_matrix\" webhook", err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn("Missing webhook permissions", err);
|
||||
}
|
||||
if (hook) {
|
||||
const hookOpts: Discord.WebhookMessageOptions & { split: true } = {
|
||||
username: asUser.displayname,
|
||||
avatarURL: asUser.avatarUrl || undefined,
|
||||
embeds: replyEmbed ? [replyEmbed] : [],
|
||||
split: true,
|
||||
};
|
||||
if (typeof sendThing === "string") {
|
||||
return await hook.send(sendThing, hookOpts);
|
||||
}
|
||||
if (sendThing instanceof Discord.MessageAttachment) {
|
||||
hookOpts.files = [sendThing];
|
||||
}
|
||||
return await hook.send(hookOpts);
|
||||
}
|
||||
log.debug("Couldn't send as webhook");
|
||||
}
|
||||
// alright, we either weren't able to send as webhook or we aren't in a webhook-able channel.
|
||||
// so.....let's try to send as embed next
|
||||
if (chan.client.user!.bot) {
|
||||
log.debug("Trying to send as embed...");
|
||||
const embed = new Discord.MessageEmbed();
|
||||
if (typeof msg === "string") {
|
||||
embed.setDescription(msg);
|
||||
} else if (msg.isImage) {
|
||||
embed.setTitle(msg.filename);
|
||||
embed.setImage(msg.url);
|
||||
} else {
|
||||
const filename = await this.discordEscape(msg.filename);
|
||||
embed.setDescription(`Uploaded a file \`${filename}\`: ${msg.url}`);
|
||||
}
|
||||
if (replyEmbed && replyEmbed.description) {
|
||||
embed.addField("Replying to", replyEmbed.author!.name);
|
||||
embed.addField("Reply text", replyEmbed.description);
|
||||
}
|
||||
embed.setAuthor(asUser.displayname, asUser.avatarUrl || undefined, `https://matrix.to/#/${asUser.mxid}`);
|
||||
return await chan.send(embed);
|
||||
}
|
||||
// alright, nothing is working....let's prefix the displayname and send stuffs
|
||||
log.debug("Prepending sender information to send the message out...");
|
||||
const displayname = await this.discordEscape(asUser.displayname);
|
||||
let sendMsg = "";
|
||||
if (typeof msg === "string") {
|
||||
sendMsg = `**${displayname}**: ${msg}`;
|
||||
} else {
|
||||
const filename = await this.discordEscape(msg.filename);
|
||||
sendMsg = `**${displayname}** uploaded a file \`${filename}\`: ${msg.url}`;
|
||||
}
|
||||
if (replyEmbed && replyEmbed.description) {
|
||||
sendMsg += `\n>>> ${replyEmbed.description}`;
|
||||
}
|
||||
return await chan.send(sendMsg);
|
||||
}
|
||||
|
||||
public async discordEscape(msg: string): Promise<string> {
|
||||
return await this.app.matrix.parseMatrixMessage(-1, {
|
||||
body: msg,
|
||||
msgtype: "m.text",
|
||||
});
|
||||
}
|
||||
|
||||
public async updatePresence(puppetId: number, presence: Discord.Presence) {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
if (!presence || !presence.user) {
|
||||
return;
|
||||
}
|
||||
const matrixPresence = {
|
||||
online: "online",
|
||||
idle: "unavailable",
|
||||
dnd: "unavailable",
|
||||
offline: "offline",
|
||||
}[presence.status] as "online" | "offline" | "unavailable";
|
||||
let statusMsg = "";
|
||||
for (const activity of presence.activities) {
|
||||
if (statusMsg !== "") {
|
||||
break;
|
||||
}
|
||||
const statusParts: string[] = [];
|
||||
if (activity.type !== "CUSTOM_STATUS") {
|
||||
const lower = activity.type.toLowerCase();
|
||||
statusParts.push(lower.charAt(0).toUpperCase() + lower.substring(1));
|
||||
if (activity.type === "LISTENING") {
|
||||
statusParts.push(`to ${activity.details} by ${activity.state}`);
|
||||
} else {
|
||||
if (activity.name) {
|
||||
statusParts.push(activity.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (activity.emoji) {
|
||||
statusParts.push(activity.emoji.name);
|
||||
}
|
||||
if (activity.state) {
|
||||
statusParts.push(activity.state);
|
||||
}
|
||||
}
|
||||
statusMsg = statusParts.join(" ");
|
||||
}
|
||||
const remoteUser = this.app.matrix.getRemoteUser(puppetId, presence.user!);
|
||||
await this.app.puppet.setUserPresence(remoteUser, matrixPresence);
|
||||
if (statusMsg) {
|
||||
await this.app.puppet.setUserStatus(remoteUser, statusMsg);
|
||||
}
|
||||
}
|
||||
|
||||
public getDiscordMsgParserCallbacks(puppetId: number): IDiscordMessageParserCallbacks {
|
||||
const p = this.app.puppets[puppetId];
|
||||
return {
|
||||
getUser: async (id: string) => {
|
||||
const mxid = await this.app.puppet.getMxidForUser({
|
||||
puppetId,
|
||||
userId: id,
|
||||
});
|
||||
let name = mxid;
|
||||
const user = await this.getUserById(p.client, id);
|
||||
if (user) {
|
||||
name = user.username;
|
||||
}
|
||||
return {
|
||||
mxid,
|
||||
name,
|
||||
};
|
||||
},
|
||||
getChannel: async (id: string) => {
|
||||
const room: IRemoteRoom = {
|
||||
puppetId,
|
||||
roomId: id,
|
||||
};
|
||||
const mxid = await this.app.puppet.getMxidForRoom(room);
|
||||
let name = mxid;
|
||||
const chan = await this.getDiscordChan(room);
|
||||
if (chan && !(chan instanceof Discord.DMChannel)) {
|
||||
name = chan.name || "";
|
||||
}
|
||||
return {
|
||||
mxid,
|
||||
name,
|
||||
};
|
||||
},
|
||||
getEmoji: async (name: string, animated: boolean, id: string) => {
|
||||
return await this.app.matrix.getEmojiMxc(puppetId, name, animated, id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async getDiscordEmoji(puppetId: number, mxc: string): Promise<Discord.GuildEmoji | null> {
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
const emote = await this.app.puppet.emoteSync.getByMxc(puppetId, mxc);
|
||||
if (!emote) {
|
||||
return null;
|
||||
}
|
||||
const emoji = p.client.emojis.resolve(emote.emoteId);
|
||||
return emoji || null;
|
||||
}
|
||||
|
||||
public async iterateGuildStructure(
|
||||
puppetId: number,
|
||||
guild: Discord.Guild,
|
||||
catCallback: (cat: Discord.CategoryChannel) => Promise<void>,
|
||||
chanCallback: (chan: BridgeableGuildChannel) => Promise<void>,
|
||||
) {
|
||||
const bridgedGuilds = await this.app.store.getBridgedGuilds(puppetId);
|
||||
const bridgedChannels = await this.app.store.getBridgedChannels(puppetId);
|
||||
const client = guild.client;
|
||||
const bridgeAll = Boolean(this.app.puppets[puppetId] && this.app.puppets[puppetId].data.bridgeAll);
|
||||
// first we iterate over the non-sorted channels
|
||||
for (const chan of guild.channels.cache.array()) {
|
||||
if (!bridgedGuilds.includes(guild.id) && !bridgedChannels.includes(chan.id) && !bridgeAll) {
|
||||
continue;
|
||||
}
|
||||
if (!chan.parentID && this.isBridgeableGuildChannel(chan) && chan.members.has(client.user!.id)) {
|
||||
await chanCallback(chan as BridgeableGuildChannel);
|
||||
}
|
||||
}
|
||||
// next we iterate over the categories and all their children
|
||||
for (const cat of guild.channels.cache.array()) {
|
||||
if (!(cat instanceof Discord.CategoryChannel)) {
|
||||
continue;
|
||||
}
|
||||
if (cat.members.has(client.user!.id)) {
|
||||
let doCat = false;
|
||||
for (const chan of cat.children.array()) {
|
||||
if (!bridgedGuilds.includes(guild.id) && !bridgedChannels.includes(chan.id) && !bridgeAll) {
|
||||
continue;
|
||||
}
|
||||
if (this.isBridgeableGuildChannel(chan) && chan.members.has(client.user!.id)) {
|
||||
if (!doCat) {
|
||||
doCat = true;
|
||||
await catCallback(cat);
|
||||
}
|
||||
await chanCallback(chan as BridgeableGuildChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserById(client: Discord.Client, id: string): Promise<Discord.User | null> {
|
||||
for (const guild of client.guilds.cache.array()) {
|
||||
const a = guild.members.cache.find((m) => m.user.id === id);
|
||||
if (a) {
|
||||
return a.user;
|
||||
}
|
||||
}
|
||||
if (client.user) {
|
||||
const user = client.user.relationships.friends.get(id);
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const user = await client.users.fetch(id);
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
} catch (err) {
|
||||
// user not found
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public isTextGuildChannel(chan?: Discord.Channel | null): boolean {
|
||||
return Boolean(chan && ["text", "news"].includes(chan.type));
|
||||
}
|
||||
|
||||
public isTextChannel(chan?: Discord.Channel | null): boolean {
|
||||
return Boolean(chan && ["text", "news", "dm", "group"].includes(chan.type));
|
||||
}
|
||||
|
||||
public isBridgeableGuildChannel(chan?: Discord.Channel | null): boolean {
|
||||
return Boolean(chan && ["text", "news"].includes(chan.type));
|
||||
}
|
||||
|
||||
public isBridgeableChannel(chan?: Discord.Channel | null): boolean {
|
||||
return Boolean(chan && ["text", "news", "dm", "group"].includes(chan.type));
|
||||
}
|
||||
}
|
||||
248
matrix-discord/mx-puppet-discord/src/index.ts
Executable file
248
matrix-discord/mx-puppet-discord/src/index.ts
Executable file
@@ -0,0 +1,248 @@
|
||||
/* tslint:disable: no-any */
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
PuppetBridge,
|
||||
IPuppetBridgeRegOpts,
|
||||
Log,
|
||||
IRetData,
|
||||
IProtocolInformation,
|
||||
} from "mx-puppet-bridge";
|
||||
import * as commandLineArgs from "command-line-args";
|
||||
import * as commandLineUsage from "command-line-usage";
|
||||
import { App } from "./app";
|
||||
|
||||
const log = new Log("DiscordPuppet:index");
|
||||
|
||||
const commandOptions = [
|
||||
{ name: "register", alias: "r", type: Boolean },
|
||||
{ name: "registration-file", alias: "f", type: String },
|
||||
{ name: "config", alias: "c", type: String },
|
||||
{ name: "help", alias: "h", type: Boolean },
|
||||
];
|
||||
const options = Object.assign({
|
||||
"register": false,
|
||||
"registration-file": "discord-registration.yaml",
|
||||
"config": "config.yaml",
|
||||
"help": false,
|
||||
}, commandLineArgs(commandOptions));
|
||||
|
||||
if (options.help) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(commandLineUsage([
|
||||
{
|
||||
header: "Matrix Discord Puppet Bridge",
|
||||
content: "A matrix puppet bridge for discord",
|
||||
},
|
||||
{
|
||||
header: "Options",
|
||||
optionList: commandOptions,
|
||||
},
|
||||
]));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const protocol: IProtocolInformation = {
|
||||
features: {
|
||||
file: true,
|
||||
presence: true,
|
||||
edit: true,
|
||||
reply: true,
|
||||
advancedRelay: true,
|
||||
globalNamespace: true,
|
||||
typingTimeout: 10 * 1000, // tslint:disable-line no-magic-numbers
|
||||
},
|
||||
id: "discord",
|
||||
displayname: "Discord",
|
||||
externalUrl: "https://discordapp.com/",
|
||||
namePatterns: {
|
||||
user: ":name",
|
||||
userOverride: ":displayname",
|
||||
room: "[:guild?#:name - :guild,:name]",
|
||||
group: ":name",
|
||||
},
|
||||
};
|
||||
|
||||
const puppet = new PuppetBridge(options["registration-file"], options.config, protocol);
|
||||
|
||||
if (options.register) {
|
||||
// okay, all we have to do is generate a registration file
|
||||
puppet.readConfig(false);
|
||||
try {
|
||||
puppet.generateRegistration({
|
||||
prefix: "_discordpuppet_",
|
||||
id: "discord-puppet",
|
||||
url: `http://${puppet.Config.bridge.bindAddress}:${puppet.Config.bridge.port}`,
|
||||
} as IPuppetBridgeRegOpts);
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log("Couldn't generate registration file:", err);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await puppet.init();
|
||||
const app = new App(puppet);
|
||||
await app.init();
|
||||
puppet.on("puppetNew", app.newPuppet.bind(app));
|
||||
puppet.on("puppetDelete", app.deletePuppet.bind(app));
|
||||
puppet.on("message", app.matrix.events.handleMatrixMessage.bind(app.matrix.events));
|
||||
puppet.on("file", app.matrix.events.handleMatrixFile.bind(app.matrix.events));
|
||||
puppet.on("redact", app.matrix.events.handleMatrixRedact.bind(app.matrix.events));
|
||||
puppet.on("edit", app.matrix.events.handleMatrixEdit.bind(app.matrix.events));
|
||||
puppet.on("reply", app.matrix.events.handleMatrixReply.bind(app.matrix.events));
|
||||
puppet.on("reaction", app.matrix.events.handleMatrixReaction.bind(app.matrix.events));
|
||||
puppet.on("removeReaction", app.matrix.events.handleMatrixRemoveReaction.bind(app.matrix.events));
|
||||
puppet.on("puppetName", app.handlePuppetName.bind(app));
|
||||
puppet.on("puppetAvatar", app.handlePuppetAvatar.bind(app));
|
||||
puppet.setGetUserIdsInRoomHook(app.getUserIdsInRoom.bind(app));
|
||||
puppet.setCreateRoomHook(app.matrix.createRoom.bind(app.matrix));
|
||||
puppet.setCreateUserHook(app.matrix.createUser.bind(app.matrix));
|
||||
puppet.setCreateGroupHook(app.matrix.createGroup.bind(app.matrix));
|
||||
puppet.setGetDmRoomIdHook(app.matrix.getDmRoom.bind(app.matrix));
|
||||
puppet.setListUsersHook(app.listUsers.bind(app));
|
||||
puppet.setListRoomsHook(app.matrix.listRooms.bind(app.matrix));
|
||||
puppet.setGetDescHook(async (puppetId: number, data: any): Promise<string> => {
|
||||
let s = "Discord";
|
||||
if (data.username) {
|
||||
s += ` as \`${data.username}\``;
|
||||
}
|
||||
if (data.id) {
|
||||
s += ` (\`${data.id}\`)`;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
puppet.setGetDataFromStrHook(async (str: string): Promise<IRetData> => {
|
||||
const retData = {
|
||||
success: false,
|
||||
} as IRetData;
|
||||
if (!str) {
|
||||
retData.error = "Please specify a token to link!";
|
||||
return retData;
|
||||
}
|
||||
const parts = str.split(" ");
|
||||
const PARTS_LENGTH = 2;
|
||||
if (parts.length !== PARTS_LENGTH) {
|
||||
retData.error = "Please specify if your token is a user or a bot token! `link <user|bot> token`";
|
||||
return retData;
|
||||
}
|
||||
const type = parts[0].toLowerCase();
|
||||
if (!["bot", "user"].includes(type)) {
|
||||
retData.error = "Please specify if your token is a user or a bot token! `link <user|bot> token`";
|
||||
return retData;
|
||||
}
|
||||
retData.success = true;
|
||||
retData.data = {
|
||||
token: parts[1].trim(),
|
||||
bot: type === "bot",
|
||||
};
|
||||
return retData;
|
||||
});
|
||||
puppet.setBotHeaderMsgHook((): string => {
|
||||
return "Discord Puppet Bridge";
|
||||
});
|
||||
puppet.setResolveRoomIdHook(async (ident: string): Promise<string | null> => {
|
||||
if (ident.match(/^[0-9]+$/)) {
|
||||
return ident;
|
||||
}
|
||||
const matches = ident.match(/^(?:https?:\/\/)?discordapp\.com\/channels\/[^\/]+\/([0-9]+)/);
|
||||
if (matches) {
|
||||
return matches[1];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
puppet.registerCommand("syncprofile", {
|
||||
fn: app.commands.commandSyncProfile.bind(app.commands),
|
||||
help: `Enable/disable the syncing of the matrix profile to the discord one (name and avatar)
|
||||
|
||||
Usage: \`syncprofile <puppetId> <1/0>\``,
|
||||
});
|
||||
puppet.registerCommand("joinentireguild", {
|
||||
fn: app.commands.commandJoinEntireGuild.bind(app.commands),
|
||||
help: `Join all the channels in a guild, if it is bridged
|
||||
|
||||
Usage: \`joinentireguild <puppetId> <guildId>\``,
|
||||
});
|
||||
puppet.registerCommand("listguilds", {
|
||||
fn: app.commands.commandListGuilds.bind(app.commands),
|
||||
help: `List all guilds that can be bridged
|
||||
|
||||
Usage: \`listguilds <puppetId>\``,
|
||||
});
|
||||
puppet.registerCommand("acceptinvite", {
|
||||
fn: app.commands.commandAcceptInvite.bind(app.commands),
|
||||
help: `Accept a discord.gg invite
|
||||
|
||||
Usage: \`acceptinvite <puppetId> <inviteLink>\``,
|
||||
});
|
||||
puppet.registerCommand("bridgeguild", {
|
||||
fn: app.commands.commandBridgeGuild.bind(app.commands),
|
||||
help: `Bridge a guild
|
||||
|
||||
Usage: \`bridgeguild <puppetId> <guildId>\``,
|
||||
});
|
||||
puppet.registerCommand("unbridgeguild", {
|
||||
fn: app.commands.commandUnbridgeGuild.bind(app.commands),
|
||||
help: `Unbridge a guild
|
||||
|
||||
Usage: \`unbridgeguild <puppetId> <guildId>\``,
|
||||
});
|
||||
puppet.registerCommand("bridgechannel", {
|
||||
fn: app.commands.commandBridgeChannel.bind(app.commands),
|
||||
help: `Bridge a channel
|
||||
|
||||
Usage: \`bridgechannel <puppetId> <channelId>\``,
|
||||
});
|
||||
puppet.registerCommand("unbridgechannel", {
|
||||
fn: app.commands.commandUnbridgeChannel.bind(app.commands),
|
||||
help: `Unbridge a channel
|
||||
|
||||
Usage: \`unbridgechannel <puppetId> <channelId>\``,
|
||||
});
|
||||
puppet.registerCommand("bridgeall", {
|
||||
fn: app.commands.commandBridgeAll.bind(app.commands),
|
||||
help: `Bridge everything
|
||||
|
||||
Usage: \`bridgeall <puppetId> <1/0>\``,
|
||||
});
|
||||
puppet.registerCommand("enablefriendsmanagement", {
|
||||
fn: app.commands.commandEnableFriendsManagement.bind(app.commands),
|
||||
help: `Enables friends management on the discord account
|
||||
|
||||
Usage: \`enablefriendsmanagement <puppetId>\``,
|
||||
});
|
||||
puppet.registerCommand("listfriends", {
|
||||
fn: app.commands.commandListFriends.bind(app.commands),
|
||||
help: `List all your current friends
|
||||
|
||||
Usage: \`listfriends <puppetId>\``,
|
||||
});
|
||||
puppet.registerCommand("addfriend", {
|
||||
fn: app.commands.commandAddFriend.bind(app.commands),
|
||||
help: `Add a new friend
|
||||
|
||||
Usage: \`addfriend <puppetId> <friend>\`, friend can be either the full username or the user ID`,
|
||||
});
|
||||
puppet.registerCommand("removefriend", {
|
||||
fn: app.commands.commandRemoveFriend.bind(app.commands),
|
||||
help: `Remove a friend
|
||||
|
||||
Usage: \`removefriend <puppetId> <friend>\`, friend can be either the full username or the user ID`,
|
||||
});
|
||||
await puppet.start();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
run(); // start the thing!
|
||||
301
matrix-discord/mx-puppet-discord/src/matrix/MatrixEventHandler.ts
Executable file
301
matrix-discord/mx-puppet-discord/src/matrix/MatrixEventHandler.ts
Executable file
@@ -0,0 +1,301 @@
|
||||
/* tslint:disable: no-any */
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { IRemoteRoom, IMessageEvent, ISendingUser, IFileEvent, Util, Log } from "mx-puppet-bridge";
|
||||
import { App, IDiscordSendFile, MAXFILESIZE, AVATAR_SETTINGS } from "../app";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { TextEncoder, TextDecoder } from "util";
|
||||
|
||||
const log = new Log("DiscordPuppet:MatrixEventHandler");
|
||||
|
||||
export class MatrixEventHandler {
|
||||
public constructor(private readonly app: App) {}
|
||||
|
||||
public async handleMatrixMessage(room: IRemoteRoom, data: IMessageEvent, asUser: ISendingUser | null, event: any) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
log.warn("Channel not found", room);
|
||||
return;
|
||||
}
|
||||
|
||||
if (asUser) {
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const displayname = (new TextEncoder().encode(asUser.displayname));
|
||||
asUser.displayname = (new TextDecoder().decode(displayname.slice(0, MAX_NAME_LENGTH)));
|
||||
}
|
||||
|
||||
const sendMsg = await this.app.matrix.parseMatrixMessage(room.puppetId, event.content);
|
||||
const lockKey = `${room.puppetId};${chan.id}`;
|
||||
this.app.messageDeduplicator.lock(lockKey, p.client.user!.id, sendMsg);
|
||||
try {
|
||||
const reply = await this.app.discord.sendToDiscord(chan, sendMsg, asUser);
|
||||
await this.app.matrix.insertNewEventId(room, data.eventId!, reply);
|
||||
} catch (err) {
|
||||
log.warn("Couldn't send message", err);
|
||||
this.app.messageDeduplicator.unlock(lockKey);
|
||||
await this.app.matrix.sendMessageFail(room);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleMatrixFile(room: IRemoteRoom, data: IFileEvent, asUser: ISendingUser | null, event: any) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
log.warn("Channel not found", room);
|
||||
return;
|
||||
}
|
||||
|
||||
let size = data.info ? data.info.size || 0 : 0;
|
||||
const mimetype = data.info ? data.info.mimetype || "" : "";
|
||||
const lockKey = `${room.puppetId};${chan.id}`;
|
||||
const isImage = Boolean(mimetype && mimetype.split("/")[0] === "image");
|
||||
if (size < MAXFILESIZE) {
|
||||
const buffer = await Util.DownloadFile(data.url);
|
||||
size = buffer.byteLength;
|
||||
if (size < MAXFILESIZE) {
|
||||
// send as attachment
|
||||
const filename = this.app.getFilenameForMedia(data.filename, mimetype);
|
||||
this.app.messageDeduplicator.lock(lockKey, p.client.user!.id, `file:${filename}`);
|
||||
try {
|
||||
const sendFile: IDiscordSendFile = {
|
||||
buffer,
|
||||
filename,
|
||||
url: data.url,
|
||||
isImage,
|
||||
};
|
||||
const reply = await this.app.discord.sendToDiscord(chan, sendFile, asUser);
|
||||
await this.app.matrix.insertNewEventId(room, data.eventId!, reply);
|
||||
return;
|
||||
} catch (err) {
|
||||
this.app.messageDeduplicator.unlock(lockKey);
|
||||
log.warn("Couldn't send media message, retrying as embed/url", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
const filename = await this.app.discord.discordEscape(data.filename);
|
||||
const msg = `Uploaded a file \`${filename}\`: ${data.url}`;
|
||||
this.app.messageDeduplicator.lock(lockKey, p.client.user!.id, msg);
|
||||
const reply = await this.app.discord.sendToDiscord(chan, msg, asUser);
|
||||
await this.app.matrix.insertNewEventId(room, data.eventId!, reply);
|
||||
} catch (err) {
|
||||
log.warn("Couldn't send media message", err);
|
||||
this.app.messageDeduplicator.unlock(lockKey);
|
||||
await this.app.matrix.sendMessageFail(room);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleMatrixRedact(room: IRemoteRoom, eventId: string, asUser: ISendingUser | null, event: any) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
log.warn("Channel not found", room);
|
||||
return;
|
||||
}
|
||||
log.verbose(`Deleting message with ID ${eventId}...`);
|
||||
const msg = await chan.messages.fetch(eventId);
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
p.deletedMessages.add(msg.id);
|
||||
await msg.delete();
|
||||
await this.app.puppet.eventSync.remove(room, msg.id);
|
||||
} catch (err) {
|
||||
log.warn("Couldn't delete message", err);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleMatrixEdit(
|
||||
room: IRemoteRoom,
|
||||
eventId: string,
|
||||
data: IMessageEvent,
|
||||
asUser: ISendingUser | null,
|
||||
event: any,
|
||||
) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
log.warn("Channel not found", room);
|
||||
return;
|
||||
}
|
||||
log.verbose(`Editing message with ID ${eventId}...`);
|
||||
const msg = await chan.messages.fetch(eventId);
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
let sendMsg = await this.app.matrix.parseMatrixMessage(room.puppetId, event.content["m.new_content"]);
|
||||
// prepend a quote, if needed
|
||||
if (msg.content.startsWith("> <")) {
|
||||
const matches = msg.content.match(/^((> [^\n]+\n)+)/);
|
||||
if (matches) {
|
||||
sendMsg = `${matches[1]}${sendMsg}`;
|
||||
}
|
||||
}
|
||||
const lockKey = `${room.puppetId};${chan.id}`;
|
||||
this.app.messageDeduplicator.lock(lockKey, p.client.user!.id, sendMsg);
|
||||
try {
|
||||
let reply: Discord.Message | Discord.Message[];
|
||||
let matrixEventId = data.eventId!;
|
||||
if (asUser) {
|
||||
// just re-send as new message
|
||||
if (eventId === this.app.lastEventIds[chan.id]) {
|
||||
try {
|
||||
p.deletedMessages.add(msg.id);
|
||||
const matrixEvents = await this.app.puppet.eventSync.getMatrix(room, msg.id);
|
||||
if (matrixEvents.length > 0) {
|
||||
matrixEventId = matrixEvents[0];
|
||||
}
|
||||
await msg.delete();
|
||||
await this.app.puppet.eventSync.remove(room, msg.id);
|
||||
} catch (err) {
|
||||
log.warn("Couldn't delete old message", err);
|
||||
}
|
||||
} else {
|
||||
sendMsg = `**EDIT:** ${sendMsg}`;
|
||||
}
|
||||
reply = await this.app.discord.sendToDiscord(chan, sendMsg, asUser);
|
||||
} else {
|
||||
reply = await msg.edit(sendMsg);
|
||||
}
|
||||
await this.app.matrix.insertNewEventId(room, matrixEventId, reply);
|
||||
} catch (err) {
|
||||
log.warn("Couldn't edit message", err);
|
||||
this.app.messageDeduplicator.unlock(lockKey);
|
||||
await this.app.matrix.sendMessageFail(room);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleMatrixReply(
|
||||
room: IRemoteRoom,
|
||||
eventId: string,
|
||||
data: IMessageEvent,
|
||||
asUser: ISendingUser | null,
|
||||
event: any,
|
||||
) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
log.warn("Channel not found", room);
|
||||
return;
|
||||
}
|
||||
log.verbose(`Replying to message with ID ${eventId}...`);
|
||||
const msg = await chan.messages.fetch(eventId);
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
let sendMsg = await this.app.matrix.parseMatrixMessage(room.puppetId, event.content);
|
||||
let content = msg.content;
|
||||
if (!content && msg.embeds.length > 0 && msg.embeds[0].description) {
|
||||
content = msg.embeds[0].description;
|
||||
}
|
||||
const quoteParts = content.split("\n");
|
||||
quoteParts[0] = `<@${msg.author.id}>: ${quoteParts[0]}`;
|
||||
const quote = quoteParts.map((s) => `> ${s}`).join("\n");
|
||||
sendMsg = `${quote}\n${sendMsg}`;
|
||||
const lockKey = `${room.puppetId};${chan.id}`;
|
||||
try {
|
||||
this.app.messageDeduplicator.lock(lockKey, p.client.user!.id, sendMsg);
|
||||
const reply = await this.app.discord.sendToDiscord(chan, sendMsg, asUser);
|
||||
await this.app.matrix.insertNewEventId(room, data.eventId!, reply);
|
||||
} catch (err) {
|
||||
log.warn("Couldn't send reply", err);
|
||||
this.app.messageDeduplicator.unlock(lockKey);
|
||||
await this.app.matrix.sendMessageFail(room);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleMatrixReaction(
|
||||
room: IRemoteRoom,
|
||||
eventId: string,
|
||||
reaction: string,
|
||||
asUser: ISendingUser | null,
|
||||
event: any,
|
||||
) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p || asUser) {
|
||||
return;
|
||||
}
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
log.warn("Channel not found", room);
|
||||
return;
|
||||
}
|
||||
log.verbose(`Reacting to ${eventId} with ${reaction}...`);
|
||||
const msg = await chan.messages.fetch(eventId);
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
if (reaction.startsWith("mxc://")) {
|
||||
const emoji = await this.app.discord.getDiscordEmoji(room.puppetId, reaction);
|
||||
if (emoji) {
|
||||
await msg.react(emoji);
|
||||
}
|
||||
} else {
|
||||
await msg.react(reaction);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleMatrixRemoveReaction(
|
||||
room: IRemoteRoom,
|
||||
eventId: string,
|
||||
reaction: string,
|
||||
asUser: ISendingUser | null,
|
||||
event: any,
|
||||
) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!p || asUser) {
|
||||
return;
|
||||
}
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
log.warn("Channel not found", room);
|
||||
return;
|
||||
}
|
||||
log.verbose(`Removing reaction to ${eventId} with ${reaction}...`);
|
||||
const msg = await chan.messages.fetch(eventId);
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
let emoji: Discord.Emoji | null = null;
|
||||
if (reaction.startsWith("mxc://")) {
|
||||
emoji = await this.app.discord.getDiscordEmoji(room.puppetId, reaction);
|
||||
}
|
||||
for (const r of msg.reactions.cache.array()) {
|
||||
if (r.emoji.name === reaction) {
|
||||
await r.remove();
|
||||
break;
|
||||
}
|
||||
if (emoji && emoji.id === r.emoji.id) {
|
||||
await r.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
404
matrix-discord/mx-puppet-discord/src/matrix/MatrixUtil.ts
Executable file
404
matrix-discord/mx-puppet-discord/src/matrix/MatrixUtil.ts
Executable file
@@ -0,0 +1,404 @@
|
||||
/* tslint:disable: no-any */
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { App, AVATAR_SETTINGS } from "../app";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { IStringFormatterVars, IRemoteUser, IRemoteRoom,
|
||||
IRemoteGroup, IRemoteUserRoomOverride, IReceiveParams, IRetList, Log,
|
||||
} from "mx-puppet-bridge";
|
||||
import * as escapeHtml from "escape-html";
|
||||
import { IMatrixMessageParserOpts } from "matrix-discord-parser";
|
||||
import { MatrixEventHandler } from "./MatrixEventHandler";
|
||||
import { TextGuildChannel, TextChannel, BridgeableGuildChannel, BridgeableChannel } from "../discord/DiscordUtil";
|
||||
|
||||
const log = new Log("DiscordPuppet:MatrixUtil");
|
||||
|
||||
export class MatrixUtil {
|
||||
public readonly events: MatrixEventHandler;
|
||||
|
||||
public constructor(private readonly app: App) {
|
||||
this.events = new MatrixEventHandler(app);
|
||||
}
|
||||
|
||||
public async getDmRoom(user: IRemoteUser): Promise<string | null> {
|
||||
const p = this.app.puppets[user.puppetId];
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
const u = await this.app.discord.getUserById(p.client, user.userId);
|
||||
if (!u) {
|
||||
return null;
|
||||
}
|
||||
return `dm-${user.puppetId}-${u.id}`;
|
||||
}
|
||||
|
||||
public async getEmojiMxc(puppetId: number, name: string, animated: boolean, id: string): Promise<string | null> {
|
||||
const emoji = await this.app.puppet.emoteSync.get({
|
||||
puppetId,
|
||||
emoteId: id,
|
||||
});
|
||||
if (emoji && emoji.avatarMxc) {
|
||||
return emoji.avatarMxc;
|
||||
}
|
||||
const { emote } = await this.app.puppet.emoteSync.set({
|
||||
puppetId,
|
||||
emoteId: id,
|
||||
avatarUrl: `https://cdn.discordapp.com/emojis/${id}${animated ? ".gif" : ".png"}`,
|
||||
name,
|
||||
data: {
|
||||
animated,
|
||||
name,
|
||||
},
|
||||
});
|
||||
return emote.avatarMxc || null;
|
||||
}
|
||||
|
||||
public getSendParams(
|
||||
puppetId: number,
|
||||
msgOrChannel: Discord.Message | BridgeableChannel,
|
||||
user?: Discord.User | Discord.GuildMember,
|
||||
): IReceiveParams {
|
||||
let channel: BridgeableChannel;
|
||||
let eventId: string | undefined;
|
||||
let externalUrl: string | undefined;
|
||||
let isWebhook = false;
|
||||
let guildChannel: BridgeableGuildChannel | undefined;
|
||||
if (!user) {
|
||||
const msg = msgOrChannel as Discord.Message;
|
||||
channel = msg.channel;
|
||||
user = msg.member || msg.author;
|
||||
eventId = msg.id;
|
||||
isWebhook = msg.webhookID ? true : false;
|
||||
if (this.app.discord.isBridgeableGuildChannel(channel)) {
|
||||
guildChannel = channel as BridgeableGuildChannel;
|
||||
externalUrl = `https://discordapp.com/channels/${guildChannel.guild.id}/${guildChannel.id}/${eventId}`;
|
||||
} else if (["group", "dm"].includes(channel.type)) {
|
||||
externalUrl = `https://discordapp.com/channels/@me/${channel.id}/${eventId}`;
|
||||
}
|
||||
} else {
|
||||
channel = msgOrChannel as BridgeableChannel;
|
||||
}
|
||||
return {
|
||||
room: this.getRemoteRoom(puppetId, channel),
|
||||
user: this.getRemoteUser(puppetId, user, isWebhook, guildChannel),
|
||||
eventId,
|
||||
externalUrl,
|
||||
};
|
||||
}
|
||||
|
||||
public getRemoteUserRoomOverride(member: Discord.GuildMember, chan: BridgeableGuildChannel): IRemoteUserRoomOverride {
|
||||
const nameVars: IStringFormatterVars = {
|
||||
name: member.user.username,
|
||||
discriminator: member.user.discriminator,
|
||||
displayname: member.displayName,
|
||||
channel: chan.name,
|
||||
guild: chan.guild.name,
|
||||
};
|
||||
return {
|
||||
nameVars,
|
||||
};
|
||||
}
|
||||
|
||||
public getRemoteUser(
|
||||
puppetId: number,
|
||||
userOrMember: Discord.User | Discord.GuildMember,
|
||||
isWebhook: boolean = false,
|
||||
chan?: BridgeableGuildChannel,
|
||||
): IRemoteUser {
|
||||
let user: Discord.User;
|
||||
let member: Discord.GuildMember | null = null;
|
||||
if (userOrMember instanceof Discord.GuildMember) {
|
||||
member = userOrMember;
|
||||
user = member.user;
|
||||
} else {
|
||||
user = userOrMember;
|
||||
}
|
||||
const nameVars: IStringFormatterVars = {
|
||||
name: user.username,
|
||||
discriminator: user.discriminator,
|
||||
};
|
||||
const response: IRemoteUser = {
|
||||
userId: isWebhook ? `webhook-${user.id}-${user.username}` : user.id,
|
||||
puppetId,
|
||||
avatarUrl: user.avatarURL(AVATAR_SETTINGS),
|
||||
nameVars,
|
||||
};
|
||||
if (member) {
|
||||
response.roomOverrides = {};
|
||||
if (chan) {
|
||||
response.roomOverrides[chan.id] = this.getRemoteUserRoomOverride(member, chan);
|
||||
} else {
|
||||
for (const gchan of member.guild.channels.cache.array()) {
|
||||
if (this.app.discord.isBridgeableGuildChannel(gchan)) {
|
||||
response.roomOverrides[gchan.id] = this.getRemoteUserRoomOverride(member, gchan as BridgeableGuildChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public getRemoteRoom(puppetId: number, channel: BridgeableChannel): IRemoteRoom {
|
||||
let roomId = channel.id;
|
||||
if (channel instanceof Discord.DMChannel) {
|
||||
roomId = `dm-${puppetId}-${channel.recipient.id}`;
|
||||
}
|
||||
const ret: IRemoteRoom = {
|
||||
roomId,
|
||||
puppetId,
|
||||
isDirect: channel.type === "dm",
|
||||
};
|
||||
if (channel instanceof Discord.GroupDMChannel) {
|
||||
ret.nameVars = {
|
||||
name: channel.name,
|
||||
};
|
||||
ret.avatarUrl = channel.iconURL(AVATAR_SETTINGS);
|
||||
}
|
||||
if (this.app.discord.isBridgeableGuildChannel(channel)) {
|
||||
const gchan = channel as BridgeableGuildChannel;
|
||||
ret.nameVars = {
|
||||
name: gchan.name,
|
||||
guild: gchan.guild.name,
|
||||
};
|
||||
ret.avatarUrl = gchan.guild.iconURL(AVATAR_SETTINGS);
|
||||
ret.groupId = gchan.guild.id;
|
||||
ret.topic = gchan.topic;
|
||||
ret.emotes = gchan.guild.emojis.cache.map((e) => {
|
||||
return {
|
||||
emoteId: e.id,
|
||||
name: e.name,
|
||||
avatarUrl: e.url,
|
||||
data: {
|
||||
animated: e.animated,
|
||||
name: e.name,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async getRemoteRoomById(room: IRemoteRoom): Promise<IRemoteRoom | null> {
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
return null;
|
||||
}
|
||||
if (!await this.app.bridgeRoom(room.puppetId, chan)) {
|
||||
return null;
|
||||
}
|
||||
return this.getRemoteRoom(room.puppetId, chan);
|
||||
}
|
||||
|
||||
public async getRemoteGroup(puppetId: number, guild: Discord.Guild): Promise<IRemoteGroup> {
|
||||
const roomIds: string[] = [];
|
||||
let description = `<h1>${escapeHtml(guild.name)}</h1>`;
|
||||
description += `<h2>Channels:</h2><ul>`;
|
||||
await this.app.discord.iterateGuildStructure(puppetId, guild,
|
||||
async (cat: Discord.CategoryChannel) => {
|
||||
const name = escapeHtml(cat.name);
|
||||
description += `</ul><h3>${name}</h3><ul>`;
|
||||
},
|
||||
async (chan: BridgeableGuildChannel) => {
|
||||
roomIds.push(chan.id);
|
||||
const mxid = await this.app.puppet.getMxidForRoom({
|
||||
puppetId,
|
||||
roomId: chan.id,
|
||||
});
|
||||
const url = "https://matrix.to/#/" + mxid;
|
||||
const name = escapeHtml(chan.name);
|
||||
description += `<li>${name}: <a href="${url}">${name}</a></li>`;
|
||||
},
|
||||
);
|
||||
description += "</ul>";
|
||||
return {
|
||||
puppetId,
|
||||
groupId: guild.id,
|
||||
nameVars: {
|
||||
name: guild.name,
|
||||
},
|
||||
avatarUrl: guild.iconURL(AVATAR_SETTINGS),
|
||||
roomIds,
|
||||
longDescription: description,
|
||||
};
|
||||
}
|
||||
|
||||
public async insertNewEventId(room: IRemoteRoom, matrixId: string, msgs: Discord.Message | Discord.Message[]) {
|
||||
const p = this.app.puppets[room.puppetId];
|
||||
if (!Array.isArray(msgs)) {
|
||||
msgs = [msgs];
|
||||
}
|
||||
for (const m of msgs) {
|
||||
const lockKey = `${room.puppetId};${m.channel.id}`;
|
||||
await this.app.puppet.eventSync.insert(room, matrixId, m.id);
|
||||
this.app.messageDeduplicator.unlock(lockKey, p.client.user!.id, m.id);
|
||||
this.app.lastEventIds[m.channel.id] = m.id;
|
||||
}
|
||||
}
|
||||
|
||||
public async createRoom(chan: IRemoteRoom): Promise<IRemoteRoom | null> {
|
||||
return await this.getRemoteRoomById(chan);
|
||||
}
|
||||
|
||||
public async createUser(user: IRemoteUser): Promise<IRemoteUser | null> {
|
||||
const p = this.app.puppets[user.puppetId];
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
if (user.userId.startsWith("webhook-")) {
|
||||
return null;
|
||||
}
|
||||
const u = await this.app.discord.getUserById(p.client, user.userId);
|
||||
if (!u) {
|
||||
return null;
|
||||
}
|
||||
const remoteUser = this.getRemoteUser(user.puppetId, u);
|
||||
remoteUser.roomOverrides = {};
|
||||
for (const guild of p.client.guilds.cache.array()) {
|
||||
const member = guild.members.resolve(u.id);
|
||||
if (member) {
|
||||
for (const chan of guild.channels.cache.array()) {
|
||||
if (this.app.discord.isBridgeableGuildChannel(chan)) {
|
||||
remoteUser.roomOverrides[chan.id] = this.getRemoteUserRoomOverride(member, chan as BridgeableGuildChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return remoteUser;
|
||||
}
|
||||
|
||||
public async createGroup(group: IRemoteGroup): Promise<IRemoteGroup | null> {
|
||||
const p = this.app.puppets[group.puppetId];
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const guild = p.client.guilds.resolve(group.groupId);
|
||||
if (!guild) {
|
||||
return null;
|
||||
}
|
||||
return await this.getRemoteGroup(group.puppetId, guild);
|
||||
}
|
||||
|
||||
public async listRooms(puppetId: number): Promise<IRetList[]> {
|
||||
const retGroups: IRetList[] = [];
|
||||
const retGuilds: IRetList[] = [];
|
||||
const p = this.app.puppets[puppetId];
|
||||
if (!p) {
|
||||
return [];
|
||||
}
|
||||
for (const guild of p.client.guilds.cache.array()) {
|
||||
let didGuild = false;
|
||||
let didCat = false;
|
||||
await this.app.discord.iterateGuildStructure(puppetId, guild,
|
||||
async (cat: Discord.CategoryChannel) => {
|
||||
didCat = true;
|
||||
retGuilds.push({
|
||||
category: true,
|
||||
name: `${guild.name} - ${cat.name}`,
|
||||
});
|
||||
},
|
||||
async (chan: BridgeableGuildChannel) => {
|
||||
if (!didGuild && !didCat) {
|
||||
didGuild = true;
|
||||
retGuilds.push({
|
||||
category: true,
|
||||
name: guild.name,
|
||||
});
|
||||
}
|
||||
retGuilds.push({
|
||||
name: chan.name,
|
||||
id: chan.id,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
for (const chan of p.client.channels.cache.array()) {
|
||||
if (chan instanceof Discord.GroupDMChannel) {
|
||||
const found = retGuilds.find((element) => element.id === chan.id);
|
||||
if (!found) {
|
||||
retGroups.push({
|
||||
name: chan.name || "",
|
||||
id: chan.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return retGroups.concat(retGuilds);
|
||||
}
|
||||
|
||||
public async parseMatrixMessage(puppetId: number, eventContent: any): Promise<string> {
|
||||
const opts: IMatrixMessageParserOpts = {
|
||||
displayname: "", // something too short
|
||||
callbacks: {
|
||||
canNotifyRoom: async () => true,
|
||||
getUserId: async (mxid: string) => {
|
||||
const parts = this.app.puppet.userSync.getPartsFromMxid(mxid);
|
||||
if (!parts || (parts.puppetId !== puppetId && parts.puppetId !== -1)) {
|
||||
return null;
|
||||
}
|
||||
return parts.userId;
|
||||
},
|
||||
getChannelId: async (mxid: string) => {
|
||||
const parts = await this.app.puppet.roomSync.getPartsFromMxid(mxid);
|
||||
if (!parts || (parts.puppetId !== puppetId && parts.puppetId !== -1)) {
|
||||
return null;
|
||||
}
|
||||
return parts.roomId;
|
||||
},
|
||||
getEmoji: async (mxc: string, name: string) => {
|
||||
const emote = await this.app.puppet.emoteSync.getByMxc(puppetId, mxc);
|
||||
log.info("Found emoji", emote);
|
||||
if (!emote) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
animated: Boolean(emote.data && emote.data.animated),
|
||||
name: ((emote.data && emote.data.name) || emote.name) as string,
|
||||
id: emote.emoteId,
|
||||
};
|
||||
},
|
||||
mxcUrlToHttp: (mxc: string) => this.app.puppet.getUrlFromMxc(mxc),
|
||||
},
|
||||
determineCodeLanguage: true,
|
||||
};
|
||||
const msg = await this.app.matrixMsgParser.FormatMessage(opts, eventContent);
|
||||
return msg;
|
||||
}
|
||||
|
||||
public async sendMessageFail(room: IRemoteRoom) {
|
||||
const chan = await this.app.discord.getDiscordChan(room);
|
||||
if (!chan) {
|
||||
return;
|
||||
}
|
||||
let msg = "";
|
||||
if (chan instanceof Discord.DMChannel) {
|
||||
msg = `Failed to send message to DM with user ${chan.recipient.username}`;
|
||||
} else if (chan instanceof Discord.GroupDMChannel) {
|
||||
let name = chan.name;
|
||||
if (!name) {
|
||||
const names: string[] = [];
|
||||
for (const user of chan.recipients.array()) {
|
||||
names.push(user.username);
|
||||
}
|
||||
name = names.join(", ");
|
||||
}
|
||||
msg = `Failed to send message into Group DM ${name}`;
|
||||
} else if (this.app.discord.isBridgeableGuildChannel(chan)) {
|
||||
const gchan = chan as BridgeableGuildChannel;
|
||||
msg = `Failed to send message into channel ${gchan.name} of guild ${gchan.guild.name}`;
|
||||
} else {
|
||||
msg = `Failed to send message into channel with id \`${chan.id}\``;
|
||||
}
|
||||
await this.app.puppet.sendStatusMessage(room, msg);
|
||||
}
|
||||
}
|
||||
108
matrix-discord/mx-puppet-discord/src/store.ts
Executable file
108
matrix-discord/mx-puppet-discord/src/store.ts
Executable file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2019, 2020 mx-puppet-discord
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Store } from "mx-puppet-bridge";
|
||||
|
||||
const CURRENT_SCHEMA = 4;
|
||||
|
||||
export class IDbEmoji {
|
||||
public emojiId: string;
|
||||
public name: string;
|
||||
public animated: boolean;
|
||||
public mxcUrl: string;
|
||||
}
|
||||
|
||||
export class DiscordStore {
|
||||
constructor(
|
||||
private store: Store,
|
||||
) { }
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.store.init(CURRENT_SCHEMA, "discord_schema", (version: number) => {
|
||||
return require(`./db/schema/v${version}.js`).Schema;
|
||||
}, false);
|
||||
}
|
||||
|
||||
public async getBridgedGuilds(puppetId: number): Promise<string[]> {
|
||||
const rows = await this.store.db.All("SELECT guild_id FROM discord_bridged_guilds WHERE puppet_id=$puppetId", {
|
||||
puppetId,
|
||||
});
|
||||
const result: string[] = [];
|
||||
for (const row of rows) {
|
||||
result.push(row.guild_id as string);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async isGuildBridged(puppetId: number, guildId: string): Promise<boolean> {
|
||||
const exists = await this.store.db.Get("SELECT 1 FROM discord_bridged_guilds WHERE puppet_id=$p AND guild_id=$g", {
|
||||
p: puppetId,
|
||||
g: guildId,
|
||||
});
|
||||
return exists ? true : false;
|
||||
}
|
||||
|
||||
public async setBridgedGuild(puppetId: number, guildId: string): Promise<void> {
|
||||
if (await this.isGuildBridged(puppetId, guildId)) {
|
||||
return;
|
||||
}
|
||||
await this.store.db.Run("INSERT INTO discord_bridged_guilds (puppet_id, guild_id) VALUES ($p, $g)", {
|
||||
p: puppetId,
|
||||
g: guildId,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeBridgedGuild(puppetId: number, guildId: string): Promise<void> {
|
||||
await this.store.db.Run("DELETE FROM discord_bridged_guilds WHERE puppet_id=$p AND guild_id=$g", {
|
||||
p: puppetId,
|
||||
g: guildId,
|
||||
});
|
||||
}
|
||||
|
||||
public async getBridgedChannels(puppetId: number): Promise<string[]> {
|
||||
const rows = await this.store.db.All("SELECT channel_id FROM discord_bridged_channels WHERE puppet_id=$puppetId", {
|
||||
puppetId,
|
||||
});
|
||||
const result: string[] = [];
|
||||
for (const row of rows) {
|
||||
result.push(row.channel_id as string);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async isChannelBridged(puppetId: number, channelId: string): Promise<boolean> {
|
||||
const exists = await this.store.db.Get("SELECT 1 FROM discord_bridged_channels" +
|
||||
" WHERE puppet_id=$p AND channel_id=$c", {
|
||||
p: puppetId,
|
||||
c: channelId,
|
||||
});
|
||||
return exists ? true : false;
|
||||
}
|
||||
|
||||
public async setBridgedChannel(puppetId: number, channelId: string): Promise<void> {
|
||||
if (await this.isChannelBridged(puppetId, channelId)) {
|
||||
return;
|
||||
}
|
||||
await this.store.db.Run("INSERT INTO discord_bridged_channels (puppet_id, channel_id) VALUES ($p, $c)", {
|
||||
p: puppetId,
|
||||
c: channelId,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeBridgedChannel(puppetId: number, channelId: string): Promise<void> {
|
||||
await this.store.db.Run("DELETE FROM discord_bridged_channels WHERE puppet_id=$p AND channel_id=$c", {
|
||||
p: puppetId,
|
||||
c: channelId,
|
||||
});
|
||||
}
|
||||
}
|
||||
17
matrix-discord/mx-puppet-discord/tsconfig.json
Executable file
17
matrix-discord/mx-puppet-discord/tsconfig.json
Executable file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2016",
|
||||
"noImplicitAny": false,
|
||||
"inlineSourceMap": true,
|
||||
"outDir": "./build",
|
||||
"types": ["node"],
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"include": [
|
||||
"src/**/*",
|
||||
]
|
||||
}
|
||||
42
matrix-discord/mx-puppet-discord/tslint.json
Executable file
42
matrix-discord/mx-puppet-discord/tslint.json
Executable file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rules": {
|
||||
"ordered-imports": false,
|
||||
"no-trailing-whitespace": "error",
|
||||
"max-classes-per-file": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"object-literal-sort-keys": "off",
|
||||
"no-any":{
|
||||
"severity": "warning"
|
||||
},
|
||||
"arrow-return-shorthand": true,
|
||||
"no-magic-numbers": [true, -1, 0, 1, 1000],
|
||||
"prefer-for-of": true,
|
||||
"typedef": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"await-promise": [true],
|
||||
"curly": true,
|
||||
"no-empty": false,
|
||||
"no-invalid-this": true,
|
||||
"no-string-throw": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"no-unused-expression": true,
|
||||
"prefer-const": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"indent": [true, "tabs", 1],
|
||||
"max-file-line-count": {
|
||||
"severity": "warning",
|
||||
"options": [500]
|
||||
},
|
||||
"no-duplicate-imports": true,
|
||||
"array-type": [true, "array"],
|
||||
"promise-function-async": true,
|
||||
"no-bitwise": true,
|
||||
"no-debugger": true,
|
||||
"no-floating-promises": true,
|
||||
"prefer-template": [true, "allow-single-concat"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user