You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
8.0 KiB
JavaScript
216 lines
8.0 KiB
JavaScript
const mx = require('matrix-appservice-bridge');
|
|
const tmi = require('tmi.js');
|
|
|
|
// has to be defined here because of scope
|
|
let bridge;
|
|
let tmiListener;
|
|
|
|
// permissions
|
|
const admins = (process.env.ADMINS || '').split(',');
|
|
console.log('admins:', admins);
|
|
const users = (process.env.USERS || '').split(',').concat(admins);
|
|
console.log('users:', users);
|
|
|
|
|
|
|
|
// Matrix part
|
|
async function checkAdminPermissions(sender) {
|
|
return admins.includes(sender);
|
|
}
|
|
async function checkUserPermissions(sender) {
|
|
if(process.env.ALLOW_ALL_USERS === 'true' || users.includes(sender)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
async function loginUser(matrixUser, remoteUser, oauthToken) {
|
|
remoteUser.set('password', oauthToken);
|
|
await bridge.getUserStore().linkUsers(matrixUser, remoteUser)
|
|
}
|
|
async function logoutUser(matrixId) {
|
|
const links = await bridge.getUserStore().getRemoteLinks(matrixId);
|
|
links.forEach(async remoteId => {
|
|
await bridge.getUserStore().unlinkUserIds(matrixId, remoteId);
|
|
console.log('Removed credentials of', remoteId, 'by', matrixId);
|
|
});
|
|
}
|
|
async function bridgeRoom(sender, matrixRoom, remoteRoom) {
|
|
await bridge.getRoomStore().linkRooms(matrixRoom, remoteRoom);
|
|
tmiListener.join(remoteRoom.roomId);
|
|
console.log('Linked rooms', matrixRoom.roomId, 'and', remoteRoom.roomId, 'by', sender);
|
|
}
|
|
async function unbridgeRoom(sender, roomId) {
|
|
await bridge.getRoomStore().removeEntriesByMatrixRoomId(roomId);
|
|
console.log('Removed Matrix room', roomId, 'by', sender);
|
|
}
|
|
async function matrixEventHandler(request, context) {
|
|
const event = request.getData();
|
|
|
|
if(!await checkUserPermissions(event.sender)) {
|
|
// no permissions; don't bridge
|
|
return;
|
|
}
|
|
|
|
if(event.type === 'm.room.message') {
|
|
const message = event.content.body;
|
|
|
|
// handle bot commands and return directly afterwards
|
|
const command = message.match(/^!tmi (?<subcommand>\w*)( (?<args>.*))?$/);
|
|
if(command) {
|
|
switch (command.groups.subcommand) {
|
|
case 'login':
|
|
const args = command.groups.args.split(' ');
|
|
const tmiUser = args[0];
|
|
const oauthToken = args[1];
|
|
await loginUser(new mx.MatrixUser(event.sender), new mx.RemoteUser(tmiUser), oauthToken);
|
|
bridge.getIntent().sendText(event.room_id, 'Saved oauth token of '+event.sender+' - please redact your login message to protect the token');
|
|
return;
|
|
case 'logout':
|
|
await logoutUser(event.sender);
|
|
bridge.getIntent().sendText(event.room_id, 'Removed oauth token of '+event.sender);
|
|
return;
|
|
case 'bridge':
|
|
if(!await checkAdminPermissions(event.sender)) {
|
|
bridge.getIntent().sendText(event.room_id, event.sender+' is not permitted as admin of this bridge');
|
|
return;
|
|
}
|
|
|
|
let channel = command.groups.args.toLowerCase();
|
|
if(!channel.match(/^#/)) {
|
|
channel = '#'+channel
|
|
}
|
|
bridgeRoom(event.sender, new mx.MatrixRoom(event.room_id), new mx.RemoteRoom(channel));
|
|
bridge.getIntent().sendText(event.room_id, 'This room is now bridged to '+channel);
|
|
return;
|
|
case 'unbridge':
|
|
if(!await checkAdminPermissions(event.sender)) {
|
|
bridge.getIntent().sendText(event.room_id, event.sender+' is not permitted as admin of this bridge');
|
|
return;
|
|
}
|
|
|
|
unbridgeRoom(event.sender, event.room_id);
|
|
bridge.getIntent().sendText(event.room_id, 'Removed all bridges from this room');
|
|
return;
|
|
default:
|
|
bridge.getIntent().sendText(event.room_id, 'available commands: login, logout, bridge, unbridge');
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
const remoteUsers = await bridge.getUserStore().getRemoteUsersFromMatrixId(event.sender);
|
|
// use the first user, multipe user mappings are unsupported
|
|
const remoteUser = remoteUsers[0];
|
|
if(!remoteUser) {
|
|
// ignore messages from users without tmi credentials
|
|
return;
|
|
}
|
|
|
|
const tmiUsername = remoteUser.id;
|
|
const tmiPassword = remoteUser.data.password;
|
|
|
|
const remoteRooms = await bridge.getRoomStore().getLinkedRemoteRooms(event.room_id);
|
|
remoteRooms.forEach(remoteRoom => {
|
|
const tmiClient = new tmi.Client({
|
|
connection: {
|
|
secure: true,
|
|
},
|
|
identity: {
|
|
username: tmiUsername,
|
|
password: tmiPassword
|
|
},
|
|
channels: [
|
|
remoteRoom.roomId
|
|
]
|
|
});
|
|
|
|
// connect to tmi, send message and disconnect
|
|
tmiClient.connect();
|
|
tmiClient.on('connected', () => {
|
|
tmiClient.say(remoteRoom.roomId, message);
|
|
tmiClient.disconnect();
|
|
});
|
|
});
|
|
} else if(event.content.membership === 'invite' && event.state_key === '@'+bridge.opts.registration.sender_localpart+':'+bridge.opts.domain) {
|
|
// auto-join on invite
|
|
bridge.getIntent().join(event.room_id);
|
|
}
|
|
};
|
|
|
|
new mx.Cli({
|
|
registrationPath: 'matritch-registration.yaml',
|
|
generateRegistration: (reg, callback) => {
|
|
reg.setId(mx.AppServiceRegistration.generateToken());
|
|
reg.setHomeserverToken(mx.AppServiceRegistration.generateToken());
|
|
reg.setAppServiceToken(mx.AppServiceRegistration.generateToken());
|
|
reg.setSenderLocalpart('_matritch');
|
|
reg.addRegexPattern('users', '@_matritch_.*', true);
|
|
callback(reg);
|
|
},
|
|
run: async (port, config) => {
|
|
const homeserver = process.env.HOMESERVER || 'http://localhost:8008';
|
|
console.log('homeserver:', homeserver);
|
|
const domain = process.env.DOMAIN || 'localhost';
|
|
console.log('domain:', domain);
|
|
|
|
bridge = new mx.Bridge({
|
|
homeserverUrl: homeserver,
|
|
domain: domain,
|
|
registration: 'matritch-registration.yaml',
|
|
controller: {
|
|
onUserQuery: queriedUser => {
|
|
// auto-provision users with no additonal data
|
|
console.log(JSON.stringify(queriedUser))
|
|
return {};
|
|
},
|
|
onEvent: matrixEventHandler
|
|
}
|
|
});
|
|
console.log('Matrix-side listening on port %s', port);
|
|
|
|
await bridge.run(port, config);
|
|
connectTmiListener();
|
|
}
|
|
}).run();
|
|
|
|
|
|
|
|
// Twitch part
|
|
const localpartPrefix = bridge.opts.registration.namespaces.users[0].regex.slice(0, -2);
|
|
const domain = bridge.opts.domain;
|
|
|
|
|
|
async function tmiEventHandler(target, context, message, self) {
|
|
const remoteUsers = await bridge.getUserStore().getRemoteUser(context.username)
|
|
if(remoteUsers) {
|
|
// ignore messages from briged users
|
|
return;
|
|
}
|
|
|
|
const matrixRooms = await bridge.getRoomStore().getLinkedMatrixRooms(target);
|
|
matrixRooms.forEach(matrixRoom => {
|
|
const intent = bridge.getIntent(localpartPrefix+context.username+':'+domain);
|
|
intent.sendText(matrixRoom.roomId, message);
|
|
});
|
|
};
|
|
async function connectTmiListener() {
|
|
const remoteRooms = await bridge.getRoomStore().select({
|
|
matrix_id: {$exists: true},
|
|
remote_id: {$exists: true}
|
|
});
|
|
const channels = remoteRooms.map(remoteRoom => {
|
|
return remoteRoom.remote_id;
|
|
});
|
|
|
|
const tmiAnonymousOptions = {
|
|
connection: {
|
|
secure: true,
|
|
reconnect: true
|
|
},
|
|
channels: channels
|
|
};
|
|
tmiListener = new tmi.Client(tmiAnonymousOptions);
|
|
tmiListener.on('message', tmiEventHandler);
|
|
tmiListener.connect();
|
|
} |