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 (?\w*)( (?.*))?$/); 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(); }