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

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();
}