forked from lug/matterbridge
		
	Compare commits
	
		
			1584 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					20f841c513 | ||
| 
						 | 
					d07a3e09c9 | ||
| 
						 | 
					4649876956 | ||
| 
						 | 
					5604d140e3 | ||
| 
						 | 
					8751fb4bb1 | ||
| 
						 | 
					3819062574 | ||
| 
						 | 
					051e6e76e9 | ||
| 
						 | 
					1e55dd47f2 | ||
| 
						 | 
					700b95546b | ||
| 
						 | 
					2fa96ec0ed | ||
| 
						 | 
					81e6f75aa4 | ||
| 
						 | 
					888c8b9a84 | ||
| 
						 | 
					e775a8a22e | ||
| 
						 | 
					99fbd9cae6 | ||
| 
						 | 
					67adad3e08 | ||
| 
						 | 
					2fca3c7563 | ||
| 
						 | 
					c3573f1a46 | ||
| 
						 | 
					ee932a9f8e | ||
| 
						 | 
					ce18c948e6 | ||
| 
						 | 
					7bc93c5506 | ||
| 
						 | 
					d7cad3b404 | ||
| 
						 | 
					7740a362c9 | ||
| 
						 | 
					281ef53e7d | ||
| 
						 | 
					f044b948e2 | ||
| 
						 | 
					32474a5f4d | ||
| 
						 | 
					26596acf80 | ||
| 
						 | 
					e63870a631 | ||
| 
						 | 
					ce782ff6fb | ||
| 
						 | 
					c6716e030c | ||
| 
						 | 
					4ab72acec6 | ||
| 
						 | 
					30aae8e257 | ||
| 
						 | 
					d7b7ff7bb4 | ||
| 
						 | 
					6fe0cff342 | ||
| 
						 | 
					5f75f9886d | ||
| 
						 | 
					5d9604cd15 | ||
| 
						 | 
					cc36ebf1c9 | ||
| 
						 | 
					e6adecfd81 | ||
| 
						 | 
					5c8f224e3b | ||
| 
						 | 
					952221d3b9 | ||
| 
						 | 
					496d5b4ec7 | ||
| 
						 | 
					2623a412c4 | ||
| 
						 | 
					d64eed49bc | ||
| 
						 | 
					fffa29c2f3 | ||
| 
						 | 
					4da1444ffc | ||
| 
						 | 
					21c4e56d16 | ||
| 
						 | 
					5356b3856a | ||
| 
						 | 
					320c996a21 | ||
| 
						 | 
					69c74be7bb | ||
| 
						 | 
					aefa70891c | ||
| 
						 | 
					1b9877fda4 | ||
| 
						 | 
					0205a67309 | ||
| 
						 | 
					e3cafeaf92 | ||
| 
						 | 
					e7b193788a | ||
| 
						 | 
					17da95b094 | ||
| 
						 | 
					c5e49eec96 | ||
| 
						 | 
					24bc0f127b | ||
| 
						 | 
					f0f801402d | ||
| 
						 | 
					663850a2b8 | ||
| 
						 | 
					c51753cab1 | ||
| 
						 | 
					b3be2e208c | ||
| 
						 | 
					c30e90ff3f | ||
| 
						 | 
					e4c0ca0f48 | ||
| 
						 | 
					9c203327c0 | ||
| 
						 | 
					ccb5b1d075 | ||
| 
						 | 
					0dbbd0414c | ||
| 
						 | 
					e7b3ebf98a | ||
| 
						 | 
					5bc18fb780 | ||
| 
						 | 
					df30366072 | ||
| 
						 | 
					65c7ac80b5 | ||
| 
						 | 
					dd3fb32ec7 | ||
| 
						 | 
					2a3f475ff5 | ||
| 
						 | 
					7288f71201 | ||
| 
						 | 
					9c43eff753 | ||
| 
						 | 
					c8d7fdeedc | ||
| 
						 | 
					c211152e23 | ||
| 
						 | 
					ab75d5097e | ||
| 
						 | 
					c3644c8d3b | ||
| 
						 | 
					6438a3dba3 | ||
| 
						 | 
					4b226a6a63 | ||
| 
						 | 
					4801850013 | ||
| 
						 | 
					6a7412bf2b | ||
| 
						 | 
					5a1fd7dadd | ||
| 
						 | 
					ac06a26809 | ||
| 
						 | 
					61d56f26f8 | ||
| 
						 | 
					6aa05b3981 | ||
| 
						 | 
					aad60c882e | ||
| 
						 | 
					fecca57507 | ||
| 
						 | 
					2bcad846c0 | ||
| 
						 | 
					15ad0165fc | ||
| 
						 | 
					2e8ab11978 | ||
| 
						 | 
					9a8ce9b17e | ||
| 
						 | 
					16ab4c6fed | ||
| 
						 | 
					e3ee0df7ba | ||
| 
						 | 
					8f7ab280e2 | ||
| 
						 | 
					dbedc99421 | ||
| 
						 | 
					6cb359cb80 | ||
| 
						 | 
					ae2ad824a9 | ||
| 
						 | 
					02e3d7852b | ||
| 
						 | 
					3893a035be | ||
| 
						 | 
					658bdd9faa | ||
| 
						 | 
					e1eebcd4e0 | ||
| 
						 | 
					062b831e88 | ||
| 
						 | 
					b275efaeff | ||
| 
						 | 
					80d3033456 | ||
| 
						 | 
					bd0516f09a | ||
| 
						 | 
					df4d76e466 | ||
| 
						 | 
					dcbd7f8cad | ||
| 
						 | 
					73ec02ab9d | ||
| 
						 | 
					d1f8347071 | ||
| 
						 | 
					8601eedada | ||
| 
						 | 
					9afd33cdfc | ||
| 
						 | 
					5e1be8e558 | ||
| 
						 | 
					835dd2635a | ||
| 
						 | 
					f65b18c2f6 | ||
| 
						 | 
					b0e7b84f40 | ||
| 
						 | 
					1635db93c7 | ||
| 
						 | 
					c4fe462d11 | ||
| 
						 | 
					b1f403165d | ||
| 
						 | 
					46e4317b77 | ||
| 
						 | 
					e3ffbcadd8 | ||
| 
						 | 
					b7d73077e5 | ||
| 
						 | 
					77f61ee20a | ||
| 
						 | 
					8967f02fc9 | ||
| 
						 | 
					831ff6d0a9 | ||
| 
						 | 
					2199174def | ||
| 
						 | 
					55f41ddaab | ||
| 
						 | 
					21305d93bf | ||
| 
						 | 
					4478d5d904 | ||
| 
						 | 
					cc6253a6b8 | ||
| 
						 | 
					85f66853bc | ||
| 
						 | 
					7464fd149c | ||
| 
						 | 
					86f1a8019c | ||
| 
						 | 
					b98d56dcf6 | ||
| 
						 | 
					a3a8a5769d | ||
| 
						 | 
					4dd8bae5c9 | ||
| 
						 | 
					7ae45c42e7 | ||
| 
						 | 
					7551b4e7a3 | ||
| 
						 | 
					61bab22dde | ||
| 
						 | 
					6dcc23ebb6 | ||
| 
						 | 
					b06a574cc5 | ||
| 
						 | 
					b56f80b1b8 | ||
| 
						 | 
					20f6c05ec5 | ||
| 
						 | 
					57fce93af7 | ||
| 
						 | 
					110b6a1431 | ||
| 
						 | 
					53cafa9f3d | ||
| 
						 | 
					d4195deb3a | ||
| 
						 | 
					400ecfb79c | ||
| 
						 | 
					86151da271 | ||
| 
						 | 
					44f3e2557d | ||
| 
						 | 
					1f365c716e | ||
| 
						 | 
					9efcc41ab2 | ||
| 
						 | 
					13bbeeaceb | ||
| 
						 | 
					da4dcec14d | ||
| 
						 | 
					761c0b79c5 | ||
| 
						 | 
					d93ab0496f | ||
| 
						 | 
					66b6f9749d | ||
| 
						 | 
					17c2d1f26a | ||
| 
						 | 
					a79e632cdc | ||
| 
						 | 
					f36498421b | ||
| 
						 | 
					e45bbe4571 | ||
| 
						 | 
					fb5a84212c | ||
| 
						 | 
					dedc1c45a1 | ||
| 
						 | 
					6a12f9ff84 | ||
| 
						 | 
					641ed1873b | ||
| 
						 | 
					1d50da4b1c | ||
| 
						 | 
					c7897cca5d | ||
| 
						 | 
					4091b6f6b4 | ||
| 
						 | 
					766f35554e | ||
| 
						 | 
					c86137449e | ||
| 
						 | 
					efec01a92f | ||
| 
						 | 
					4fcad8e04b | ||
| 
						 | 
					4b4b2d790e | ||
| 
						 | 
					ec6ae343dd | ||
| 
						 | 
					b9fb361959 | ||
| 
						 | 
					a189298ab0 | ||
| 
						 | 
					714a2ad730 | ||
| 
						 | 
					fa8b96dfa1 | ||
| 
						 | 
					01955a0df8 | ||
| 
						 | 
					ac4aee39e3 | ||
| 
						 | 
					a0bca42a7a | ||
| 
						 | 
					af543dcd05 | ||
| 
						 | 
					af77109a47 | ||
| 
						 | 
					b979aff270 | ||
| 
						 | 
					b293e3fa75 | ||
| 
						 | 
					21eb37e471 | ||
| 
						 | 
					d3b60cc445 | ||
| 
						 | 
					7466e1d014 | ||
| 
						 | 
					2a7f28606c | ||
| 
						 | 
					0450482e6e | ||
| 
						 | 
					ee5d9b43b5 | ||
| 
						 | 
					3a8857c8c9 | ||
| 
						 | 
					be3dfb251d | ||
| 
						 | 
					4e11e29f70 | ||
| 
						 | 
					763bb95cea | ||
| 
						 | 
					668e7407e6 | ||
| 
						 | 
					c147ba1da1 | ||
| 
						 | 
					10f044c3dd | ||
| 
						 | 
					ce5140febd | ||
| 
						 | 
					858cdc86f5 | ||
| 
						 | 
					9a25297d51 | ||
| 
						 | 
					e24f7f5151 | ||
| 
						 | 
					eff5f1e119 | ||
| 
						 | 
					afcd362cd1 | ||
| 
						 | 
					0452be0cb3 | ||
| 
						 | 
					1624f10773 | ||
| 
						 | 
					8764be7461 | ||
| 
						 | 
					5dd15ef8e7 | ||
| 
						 | 
					4ac6366706 | ||
| 
						 | 
					adc0912efa | ||
| 
						 | 
					536823ce55 | ||
| 
						 | 
					207cd24edb | ||
| 
						 | 
					b039da1eba | ||
| 
						 | 
					8fcd0f3b6f | ||
| 
						 | 
					16fde6935c | ||
| 
						 | 
					9592cff9fa | ||
| 
						 | 
					109148988c | ||
| 
						 | 
					cf13fff7d2 | ||
| 
						 | 
					a9d8ac8bc0 | ||
| 
						 | 
					1a4717b366 | ||
| 
						 | 
					6cadf12260 | ||
| 
						 | 
					19d47784bd | ||
| 
						 | 
					b89102c5fc | ||
| 
						 | 
					4f20ebead3 | ||
| 
						 | 
					a9f89dbc64 | ||
| 
						 | 
					58ea1e07d2 | ||
| 
						 | 
					6de4c7e971 | ||
| 
						 | 
					03dc51ffa2 | ||
| 
						 | 
					aef2dcdfdd | ||
| 
						 | 
					0494119bf4 | ||
| 
						 | 
					0a17e21119 | ||
| 
						 | 
					52e2f926f4 | ||
| 
						 | 
					611fb279bc | ||
| 
						 | 
					41b4e64be9 | ||
| 
						 | 
					0d7315249d | ||
| 
						 | 
					4913766d58 | ||
| 
						 | 
					92da8c7044 | ||
| 
						 | 
					9dba3d5385 | ||
| 
						 | 
					2d3c26a4b2 | ||
| 
						 | 
					8eba2d3e50 | ||
| 
						 | 
					a8d4a27de1 | ||
| 
						 | 
					c42167c6f4 | ||
| 
						 | 
					44d182e2f9 | ||
| 
						 | 
					ad95e35687 | ||
| 
						 | 
					640a9995f4 | ||
| 
						 | 
					95625f6871 | ||
| 
						 | 
					2c20f72a9c | ||
| 
						 | 
					5ad788e768 | ||
| 
						 | 
					ed98c586c6 | ||
| 
						 | 
					3e865708d6 | ||
| 
						 | 
					c3bcbd63c0 | ||
| 
						 | 
					29e29439ee | ||
| 
						 | 
					0c19716f44 | ||
| 
						 | 
					b24e1bafa1 | ||
| 
						 | 
					64b899ac89 | ||
| 
						 | 
					aa274e5ab7 | ||
| 
						 | 
					7b3eaf3ccf | ||
| 
						 | 
					1a5353d768 | ||
| 
						 | 
					eff41759bc | ||
| 
						 | 
					c23252ab53 | ||
| 
						 | 
					1a3c57a031 | ||
| 
						 | 
					4cc2c914e6 | ||
| 
						 | 
					cbb46293ab | ||
| 
						 | 
					765e00c949 | ||
| 
						 | 
					662359908b | ||
| 
						 | 
					0d99766686 | ||
| 
						 | 
					ae3bc3358b | ||
| 
						 | 
					1e0b4532bd | ||
| 
						 | 
					4f8b19c686 | ||
| 
						 | 
					84ab223b81 | ||
| 
						 | 
					2bb21262d4 | ||
| 
						 | 
					3188a9ffe6 | ||
| 
						 | 
					61569a8610 | ||
| 
						 | 
					075a84427f | ||
| 
						 | 
					950f2759bd | ||
| 
						 | 
					25c82ddf02 | ||
| 
						 | 
					2d98df6122 | ||
| 
						 | 
					219a5453f9 | ||
| 
						 | 
					214a6a1386 | ||
| 
						 | 
					e7781dc79c | ||
| 
						 | 
					10c4bd1ac8 | ||
| 
						 | 
					a42e488e58 | ||
| 
						 | 
					06eb89b05b | ||
| 
						 | 
					91c58ec027 | ||
| 
						 | 
					8b26e42a3a | ||
| 
						 | 
					acca011f15 | ||
| 
						 | 
					2f59abdda7 | ||
| 
						 | 
					17747a5c88 | ||
| 
						 | 
					cec63546ff | ||
| 
						 | 
					75f67d2de4 | ||
| 
						 | 
					712385ffd5 | ||
| 
						 | 
					ad90cf09fe | ||
| 
						 | 
					f9928c9e25 | ||
| 
						 | 
					a0741d99b8 | ||
| 
						 | 
					c63f08c811 | ||
| 
						 | 
					58b6c4d277 | ||
| 
						 | 
					27c02549c8 | ||
| 
						 | 
					88d371c71c | ||
| 
						 | 
					b339524613 | ||
| 
						 | 
					d5feda5c8a | ||
| 
						 | 
					2f506425c2 | ||
| 
						 | 
					e8167ee3d7 | ||
| 
						 | 
					2f5e211065 | ||
| 
						 | 
					39f4fb3446 | ||
| 
						 | 
					56159b9bce | ||
| 
						 | 
					b2af76e7dc | ||
| 
						 | 
					491fe35397 | ||
| 
						 | 
					b451285af7 | ||
| 
						 | 
					63a1847cdc | ||
| 
						 | 
					4e50fd8649 | ||
| 
						 | 
					dfdffa0027 | ||
| 
						 | 
					ebd2073144 | ||
| 
						 | 
					1e94b716fb | ||
| 
						 | 
					2a41abb3d1 | ||
| 
						 | 
					2d2bebe976 | ||
| 
						 | 
					e1629994bd | ||
| 
						 | 
					e3d8fe4fd8 | ||
| 
						 | 
					23d8742f0d | ||
| 
						 | 
					3b6a8be07b | ||
| 
						 | 
					71a5b72aff | ||
| 
						 | 
					213bf349c3 | ||
| 
						 | 
					a94fe55886 | ||
| 
						 | 
					9b22f16497 | ||
| 
						 | 
					2977a5957e | ||
| 
						 | 
					f70d1c897a | ||
| 
						 | 
					a4a3525265 | ||
| 
						 | 
					a6dd8446e4 | ||
| 
						 | 
					7bf9e1cfb3 | ||
| 
						 | 
					f291832a77 | ||
| 
						 | 
					1fee323247 | ||
| 
						 | 
					a41accd033 | ||
| 
						 | 
					37f7caf7f3 | ||
| 
						 | 
					5847f7758c | ||
| 
						 | 
					bce736993e | ||
| 
						 | 
					5636992446 | ||
| 
						 | 
					f996a2b7ae | ||
| 
						 | 
					587de96ab3 | ||
| 
						 | 
					80eb1cd202 | ||
| 
						 | 
					bbf594c815 | ||
| 
						 | 
					2f0f2ee40d | ||
| 
						 | 
					96022d3aaf | ||
| 
						 | 
					8eb5e3cbf8 | ||
| 
						 | 
					ddc2625934 | ||
| 
						 | 
					7f7ca697a0 | ||
| 
						 | 
					900375679b | ||
| 
						 | 
					9440b9e313 | ||
| 
						 | 
					393f9e998b | ||
| 
						 | 
					ba0bfe70a8 | ||
| 
						 | 
					3c4a3e3f75 | ||
| 
						 | 
					274fb09ed4 | ||
| 
						 | 
					d44598a900 | ||
| 
						 | 
					c9cfa59f54 | ||
| 
						 | 
					7062234331 | ||
| 
						 | 
					9754569525 | ||
| 
						 | 
					52a071e34d | ||
| 
						 | 
					2d8f749e36 | ||
| 
						 | 
					a18cb74f03 | ||
| 
						 | 
					6c442e239d | ||
| 
						 | 
					eaf92fca4d | ||
| 
						 | 
					06b7bad714 | ||
| 
						 | 
					19eec2ed03 | ||
| 
						 | 
					d99c54343a | ||
| 
						 | 
					308a110000 | ||
| 
						 | 
					4f406b2ce6 | ||
| 
						 | 
					e564c555d7 | ||
| 
						 | 
					f7ec9af9e8 | ||
| 
						 | 
					4d93a774ce | ||
| 
						 | 
					2595dd30bf | ||
| 
						 | 
					9190365289 | ||
| 
						 | 
					57794b3b9f | ||
| 
						 | 
					3c36f651be | ||
| 
						 | 
					8e6ddadba2 | ||
| 
						 | 
					8a87a71927 | ||
| 
						 | 
					0047e6f523 | ||
| 
						 | 
					7183095a28 | ||
| 
						 | 
					13c90893c7 | ||
| 
						 | 
					976fbcd07f | ||
| 
						 | 
					d97b077e85 | ||
| 
						 | 
					8950575bfb | ||
| 
						 | 
					11fc4c286f | ||
| 
						 | 
					8d08e348a9 | ||
| 
						 | 
					a18807f19e | ||
| 
						 | 
					29f658fd3c | ||
| 
						 | 
					a30bb8fed0 | ||
| 
						 | 
					092ca1cd67 | ||
| 
						 | 
					0df2539641 | ||
| 
						 | 
					0f2d8a599c | ||
| 
						 | 
					54b3143a1d | ||
| 
						 | 
					148f7d2a91 | ||
| 
						 | 
					1aa662f763 | ||
| 
						 | 
					0b86b88de7 | ||
| 
						 | 
					98033b1ba7 | ||
| 
						 | 
					2b7eab629d | ||
| 
						 | 
					0e4973e15c | ||
| 
						 | 
					af0acf0dae | ||
| 
						 | 
					76e5fe5a87 | ||
| 
						 | 
					802c80f40c | ||
| 
						 | 
					a51c5bd905 | ||
| 
						 | 
					8c68556f52 | ||
| 
						 | 
					cca1ea2404 | ||
| 
						 | 
					281016a501 | ||
| 
						 | 
					d4acdf2f89 | ||
| 
						 | 
					0951e75c85 | ||
| 
						 | 
					6b017b226a | ||
| 
						 | 
					9e3bd7398c | ||
| 
						 | 
					79f764c7a8 | ||
| 
						 | 
					b5dc4353fb | ||
| 
						 | 
					2fbac73c29 | ||
| 
						 | 
					6616d105d1 | ||
| 
						 | 
					6b4b19194e | ||
| 
						 | 
					9785edd263 | ||
| 
						 | 
					2a0bc11b68 | ||
| 
						 | 
					dd0325a88d | ||
| 
						 | 
					20783c0978 | ||
| 
						 | 
					3f06a40bd5 | ||
| 
						 | 
					68f43985ad | ||
| 
						 | 
					915ca8f817 | ||
| 
						 | 
					a65a81610b | ||
| 
						 | 
					8eb6ed5639 | ||
| 
						 | 
					795a8705c3 | ||
| 
						 | 
					3af0dc3b3a | ||
| 
						 | 
					9cf9b958a3 | ||
| 
						 | 
					3ac2ba8d5a | ||
| 
						 | 
					d893421c7b | ||
| 
						 | 
					250b3bb579 | ||
| 
						 | 
					e9edbfc051 | ||
| 
						 | 
					e343db6f72 | ||
| 
						 | 
					4d57d66f85 | ||
| 
						 | 
					54ed6320c2 | ||
| 
						 | 
					23083f3ae0 | ||
| 
						 | 
					1985873494 | ||
| 
						 | 
					8ae5917659 | ||
| 
						 | 
					c91bfd08d8 | ||
| 
						 | 
					49110a5872 | ||
| 
						 | 
					c01c8edeb8 | ||
| 
						 | 
					ff8cf067b8 | ||
| 
						 | 
					1420f68050 | ||
| 
						 | 
					c0be3e585a | ||
| 
						 | 
					3049ef9151 | ||
| 
						 | 
					4be00bbe6b | ||
| 
						 | 
					9382dde098 | ||
| 
						 | 
					1bf46b7711 | ||
| 
						 | 
					b85bae31d9 | ||
| 
						 | 
					0898829313 | ||
| 
						 | 
					f8ad877601 | ||
| 
						 | 
					585d1556c1 | ||
| 
						 | 
					7486555875 | ||
| 
						 | 
					fc30b1bacc | ||
| 
						 | 
					0dd19af6e8 | ||
| 
						 | 
					4c44515f9d | ||
| 
						 | 
					9d84d6dd64 | ||
| 
						 | 
					0f708daf2d | ||
| 
						 | 
					b9354de8fd | ||
| 
						 | 
					c9d5f4c898 | ||
| 
						 | 
					810c150781 | ||
| 
						 | 
					31dd538c0b | ||
| 
						 | 
					62e38e7c45 | ||
| 
						 | 
					b9da28a29b | ||
| 
						 | 
					84bfa8a6b1 | ||
| 
						 | 
					1f830963f6 | ||
| 
						 | 
					12d2c6fe89 | ||
| 
						 | 
					f43faf15f8 | ||
| 
						 | 
					173a38a374 | ||
| 
						 | 
					1604ff15b5 | ||
| 
						 | 
					214fe502cd | ||
| 
						 | 
					aae45a8179 | ||
| 
						 | 
					075ca9ca47 | ||
| 
						 | 
					d4253d7a55 | ||
| 
						 | 
					0917dc8766 | ||
| 
						 | 
					aba86855b5 | ||
| 
						 | 
					ed5386c213 | ||
| 
						 | 
					455e75e92f | ||
| 
						 | 
					c394de0c88 | ||
| 
						 | 
					bad1990173 | ||
| 
						 | 
					0bc159341d | ||
| 
						 | 
					45bf1fd63a | ||
| 
						 | 
					ff0de85817 | ||
| 
						 | 
					727fa9f929 | ||
| 
						 | 
					0b9bc18236 | ||
| 
						 | 
					bad3b83d33 | ||
| 
						 | 
					00967a98ac | ||
| 
						 | 
					1d708ab351 | ||
| 
						 | 
					ba6759010b | ||
| 
						 | 
					da3868c104 | ||
| 
						 | 
					0abf4d5d5d | ||
| 
						 | 
					9b320cd43f | ||
| 
						 | 
					28783a4146 | ||
| 
						 | 
					f92927eae5 | ||
| 
						 | 
					294139ce7a | ||
| 
						 | 
					45becd2573 | ||
| 
						 | 
					a3bee01e0a | ||
| 
						 | 
					1dc93ec4f0 | ||
| 
						 | 
					3562d4220c | ||
| 
						 | 
					1532f6e427 | ||
| 
						 | 
					9327810bbf | ||
| 
						 | 
					f66d5f1e58 | ||
| 
						 | 
					cec086994e | ||
| 
						 | 
					942d8f1ced | ||
| 
						 | 
					1552dcb143 | ||
| 
						 | 
					d525f1c9e4 | ||
| 
						 | 
					921f2dfcdf | ||
| 
						 | 
					79a006c8de | ||
| 
						 | 
					ff27746c0c | ||
| 
						 | 
					87788f354f | ||
| 
						 | 
					7d2e440c83 | ||
| 
						 | 
					5551f9d56f | ||
| 
						 | 
					1fb91c6316 | ||
| 
						 | 
					e60949ff3f | ||
| 
						 | 
					278a3c6890 | ||
| 
						 | 
					fcf734eb36 | ||
| 
						 | 
					cf3cddafab | ||
| 
						 | 
					c52664f22e | ||
| 
						 | 
					cb712ff37d | ||
| 
						 | 
					f4ae610448 | ||
| 
						 | 
					601b8bc98d | ||
| 
						 | 
					80b4cec87a | ||
| 
						 | 
					76c7b69e4e | ||
| 
						 | 
					a5bd3c4dda | ||
| 
						 | 
					f06e9b5605 | ||
| 
						 | 
					7a3bb0e55c | ||
| 
						 | 
					6e8f535e8b | ||
| 
						 | 
					5619a75b05 | ||
| 
						 | 
					53dfb78215 | ||
| 
						 | 
					8e97cbab1e | ||
| 
						 | 
					ce7b749fd5 | ||
| 
						 | 
					6617bd6609 | ||
| 
						 | 
					e610fb3201 | ||
| 
						 | 
					40f1d35415 | ||
| 
						 | 
					b79bf7d414 | ||
| 
						 | 
					3724cc3a15 | ||
| 
						 | 
					3418e8c9af | ||
| 
						 | 
					9619dff334 | ||
| 
						 | 
					1b2feb19e5 | ||
| 
						 | 
					1829dc3d9f | ||
| 
						 | 
					bd0e81f5a0 | ||
| 
						 | 
					f04d360ee2 | ||
| 
						 | 
					92f27281fa | ||
| 
						 | 
					65781b9316 | ||
| 
						 | 
					9be0be0316 | ||
| 
						 | 
					9f5f004725 | ||
| 
						 | 
					fed77cccf3 | ||
| 
						 | 
					9b520dfb78 | ||
| 
						 | 
					8ad2be10b2 | ||
| 
						 | 
					2d277a15f5 | ||
| 
						 | 
					d60468bb05 | ||
| 
						 | 
					82d6210464 | ||
| 
						 | 
					ff198042d2 | ||
| 
						 | 
					6b47e29583 | ||
| 
						 | 
					380c38674c | ||
| 
						 | 
					3c14a0891e | ||
| 
						 | 
					8513a07416 | ||
| 
						 | 
					220485a849 | ||
| 
						 | 
					4db34b0506 | ||
| 
						 | 
					5677c912a8 | ||
| 
						 | 
					7a24de15e4 | ||
| 
						 | 
					99d9ea283a | ||
| 
						 | 
					dac92a0e0a | ||
| 
						 | 
					a25efb16f3 | ||
| 
						 | 
					e4d73b29a1 | ||
| 
						 | 
					8a875f292e | ||
| 
						 | 
					60a85621ea | ||
| 
						 | 
					115d20373c | ||
| 
						 | 
					cdf33e5748 | ||
| 
						 | 
					01d0a9f412 | ||
| 
						 | 
					8cc2d3b4fe | ||
| 
						 | 
					aba9e4f3be | ||
| 
						 | 
					4d575ba13a | ||
| 
						 | 
					7f0e4ad448 | ||
| 
						 | 
					17cc14a9d2 | ||
| 
						 | 
					1f8016182c | ||
| 
						 | 
					caf9ef2c4b | ||
| 
						 | 
					64b57f2da3 | ||
| 
						 | 
					efd2c99862 | ||
| 
						 | 
					cc05ba8907 | ||
| 
						 | 
					16763b715a | ||
| 
						 | 
					ffaa598796 | ||
| 
						 | 
					858e16d34f | ||
| 
						 | 
					a60e62efb1 | ||
| 
						 | 
					97f9d4be67 | ||
| 
						 | 
					fa4eec41f7 | ||
| 
						 | 
					77516c97db | ||
| 
						 | 
					cba01f0865 | ||
| 
						 | 
					8b754017ca | ||
| 
						 | 
					a27600046e | ||
| 
						 | 
					fb2667631d | ||
| 
						 | 
					b638f7037a | ||
| 
						 | 
					74699a8262 | ||
| 
						 | 
					eabf2a4582 | ||
| 
						 | 
					325d62b41c | ||
| 
						 | 
					e955a056e2 | ||
| 
						 | 
					723f8c5fd5 | ||
| 
						 | 
					a16137f53f | ||
| 
						 | 
					d60b8b97f9 | ||
| 
						 | 
					7b0bc51183 | ||
| 
						 | 
					53aa076555 | ||
| 
						 | 
					f57370f33a | ||
| 
						 | 
					c557d51b6f | ||
| 
						 | 
					df3fdc26a0 | ||
| 
						 | 
					af00c34aac | ||
| 
						 | 
					120bf39f55 | ||
| 
						 | 
					26a7e35f27 | ||
| 
						 | 
					d44d2a5f00 | ||
| 
						 | 
					7f1d86b338 | ||
| 
						 | 
					d8816280f0 | ||
| 
						 | 
					b09a73040f | ||
| 
						 | 
					740b5f2602 | ||
| 
						 | 
					96841c70c7 | ||
| 
						 | 
					f92735d35d | ||
| 
						 | 
					516fd3c92d | ||
| 
						 | 
					a775b57134 | ||
| 
						 | 
					bf21604d42 | ||
| 
						 | 
					1bb39eba87 | ||
| 
						 | 
					3190703dc8 | ||
| 
						 | 
					5095db8a43 | ||
| 
						 | 
					1f1634ea59 | ||
| 
						 | 
					a7dd033c3b | ||
| 
						 | 
					95e78ffa05 | ||
| 
						 | 
					42276ea7d0 | ||
| 
						 | 
					dffd67eb31 | ||
| 
						 | 
					55e79063d6 | ||
| 
						 | 
					46f4bbb3b5 | ||
| 
						 | 
					240559581a | ||
| 
						 | 
					48ba829465 | ||
| 
						 | 
					eef654de98 | ||
| 
						 | 
					d76a04bd0a | ||
| 
						 | 
					a8fe54a78d | ||
| 
						 | 
					0bcb0b882f | ||
| 
						 | 
					4525fa31aa | ||
| 
						 | 
					aeaea0574f | ||
| 
						 | 
					99d71c2177 | ||
| 
						 | 
					3e60cfafd3 | ||
| 
						 | 
					3123695869 | ||
| 
						 | 
					777af73e2b | ||
| 
						 | 
					716751cf76 | ||
| 
						 | 
					6ebd5cbbd8 | ||
| 
						 | 
					077b818d82 | ||
| 
						 | 
					5af1d80055 | ||
| 
						 | 
					f236d12166 | ||
| 
						 | 
					127eb908f3 | ||
| 
						 | 
					40d76b2296 | ||
| 
						 | 
					8147815037 | ||
| 
						 | 
					57f156be83 | ||
| 
						 | 
					2cfd880cdb | ||
| 
						 | 
					430b38e770 | ||
| 
						 | 
					e7f463a082 | ||
| 
						 | 
					1d39c771e4 | ||
| 
						 | 
					c81c0dd22a | ||
| 
						 | 
					f8a1ab4622 | ||
| 
						 | 
					5d3309fdcd | ||
| 
						 | 
					4ae028fe73 | ||
| 
						 | 
					707db950c8 | ||
| 
						 | 
					94812d8648 | ||
| 
						 | 
					8548b69e6e | ||
| 
						 | 
					e3cb665d92 | ||
| 
						 | 
					fb713ed91b | ||
| 
						 | 
					d99eacc2e1 | ||
| 
						 | 
					62e55214fc | ||
| 
						 | 
					464d27ad7e | ||
| 
						 | 
					f88c5f6c08 | ||
| 
						 | 
					da6ce791bc | ||
| 
						 | 
					b33b50987b | ||
| 
						 | 
					5193634a52 | ||
| 
						 | 
					0d94746f4a | ||
| 
						 | 
					85680935d4 | ||
| 
						 | 
					46e2683995 | ||
| 
						 | 
					492722af8b | ||
| 
						 | 
					56749dfb20 | ||
| 
						 | 
					04567c765e | ||
| 
						 | 
					048158ad6d | ||
| 
						 | 
					7326b9e10d | ||
| 
						 | 
					8522d8f29c | ||
| 
						 | 
					bab385c342 | ||
| 
						 | 
					bb27ef7939 | ||
| 
						 | 
					d2044c647b | ||
| 
						 | 
					c585d00f16 | ||
| 
						 | 
					015c076315 | ||
| 
						 | 
					426aa33723 | ||
| 
						 | 
					da8e415ae1 | ||
| 
						 | 
					1b834c6858 | ||
| 
						 | 
					d82726cd1b | ||
| 
						 | 
					288f0a06bb | ||
| 
						 | 
					0121d75032 | ||
| 
						 | 
					53c86702a3 | ||
| 
						 | 
					192fe89789 | ||
| 
						 | 
					959ca3cef3 | ||
| 
						 | 
					ccd55d2a28 | ||
| 
						 | 
					bfa9a83d31 | ||
| 
						 | 
					2f7b4d7f68 | ||
| 
						 | 
					d887855e16 | ||
| 
						 | 
					1a1e68ec98 | ||
| 
						 | 
					a2754f15fc | ||
| 
						 | 
					b6d81f34ba | ||
| 
						 | 
					f9fb33e696 | ||
| 
						 | 
					f72d5de2d7 | ||
| 
						 | 
					0365c0786a | ||
| 
						 | 
					af7a00d030 | ||
| 
						 | 
					8a7efce941 | ||
| 
						 | 
					ce73aa5a74 | ||
| 
						 | 
					64d63a25cc | ||
| 
						 | 
					2cef9c4fcf | ||
| 
						 | 
					4265d43096 | ||
| 
						 | 
					25857591a2 | ||
| 
						 | 
					27f5a1a685 | ||
| 
						 | 
					84da2d6a29 | ||
| 
						 | 
					859ebad55d | ||
| 
						 | 
					e538a4d304 | ||
| 
						 | 
					f94c2b40a3 | ||
| 
						 | 
					47d29ecf63 | ||
| 
						 | 
					f2088a687e | ||
| 
						 | 
					faeeee2948 | ||
| 
						 | 
					7923cfe8f8 | ||
| 
						 | 
					b51d0a9b05 | ||
| 
						 | 
					09f22a801e | ||
| 
						 | 
					3a824c5f9d | ||
| 
						 | 
					df02f51c56 | ||
| 
						 | 
					fc5e3a6728 | ||
| 
						 | 
					57fbd3c723 | ||
| 
						 | 
					25cd1e2cc1 | ||
| 
						 | 
					f5659d455d | ||
| 
						 | 
					5ed7abdbeb | ||
| 
						 | 
					09875fe160 | ||
| 
						 | 
					f716b8fc0f | ||
| 
						 | 
					9f66f93641 | ||
| 
						 | 
					f00d4d7d3f | ||
| 
						 | 
					0929535b2e | ||
| 
						 | 
					8869e253ca | ||
| 
						 | 
					f3a5ea2956 | ||
| 
						 | 
					f4d4dc91b1 | ||
| 
						 | 
					c6fd65d1d7 | ||
| 
						 | 
					0795906533 | ||
| 
						 | 
					a2b45bc799 | ||
| 
						 | 
					757657f29c | ||
| 
						 | 
					219c7659e1 | ||
| 
						 | 
					ae32bae791 | ||
| 
						 | 
					57eba77561 | ||
| 
						 | 
					d5bc7c4343 | ||
| 
						 | 
					32f57b7c26 | ||
| 
						 | 
					692bb8faa7 | ||
| 
						 | 
					455a0fc239 | ||
| 
						 | 
					b2cbd13251 | ||
| 
						 | 
					ce21ba1545 | ||
| 
						 | 
					c89085bf44 | ||
| 
						 | 
					4254ed3c63 | ||
| 
						 | 
					85564a35fd | ||
| 
						 | 
					09713d40ba | ||
| 
						 | 
					16d5aeac7c | ||
| 
						 | 
					e19ba5a06a | ||
| 
						 | 
					f7a5077d5d | ||
| 
						 | 
					f8dc24bc09 | ||
| 
						 | 
					e9419f10d3 | ||
| 
						 | 
					cded603c27 | ||
| 
						 | 
					d2ae3ebf9e | ||
| 
						 | 
					730ccdd456 | ||
| 
						 | 
					2f042ad915 | ||
| 
						 | 
					ba70691877 | ||
| 
						 | 
					ed11686a99 | ||
| 
						 | 
					5c50d86908 | ||
| 
						 | 
					fea31753b0 | ||
| 
						 | 
					0d64cd8bab | ||
| 
						 | 
					9be0f8f000 | ||
| 
						 | 
					78401214b0 | ||
| 
						 | 
					b2a07aba3a | ||
| 
						 | 
					1e0bb3da95 | ||
| 
						 | 
					59994da176 | ||
| 
						 | 
					3d281b3316 | ||
| 
						 | 
					ea86849a58 | ||
| 
						 | 
					399789811e | ||
| 
						 | 
					8d117cb0a4 | ||
| 
						 | 
					588b8e0303 | ||
| 
						 | 
					1794922263 | ||
| 
						 | 
					0ededb8863 | ||
| 
						 | 
					aa59bb1a41 | ||
| 
						 | 
					f2703979a4 | ||
| 
						 | 
					d2a1dc792f | ||
| 
						 | 
					06d66a0b2b | ||
| 
						 | 
					0e2522279e | ||
| 
						 | 
					141a42a75b | ||
| 
						 | 
					a1bf37e457 | ||
| 
						 | 
					a20b7895a9 | ||
| 
						 | 
					5666821e7b | ||
| 
						 | 
					5132d8f097 | ||
| 
						 | 
					b81ff9c008 | ||
| 
						 | 
					7e62bc4819 | ||
| 
						 | 
					d058be25ad | ||
| 
						 | 
					1269be1d04 | ||
| 
						 | 
					3b8837a16b | ||
| 
						 | 
					32f478e4a0 | ||
| 
						 | 
					e2b50d6194 | ||
| 
						 | 
					74e33b0a51 | ||
| 
						 | 
					107969c09a | ||
| 
						 | 
					d379118772 | ||
| 
						 | 
					291594b99c | ||
| 
						 | 
					f2cdda7278 | ||
| 
						 | 
					6911458d15 | ||
| 
						 | 
					6238effdc2 | ||
| 
						 | 
					498377a230 | ||
| 
						 | 
					3dd4ec57ff | ||
| 
						 | 
					e15b0e04b8 | ||
| 
						 | 
					97b1fc813b | ||
| 
						 | 
					917040b044 | ||
| 
						 | 
					69646a160d | ||
| 
						 | 
					54adb0509e | ||
| 
						 | 
					bd3a3b6eaf | ||
| 
						 | 
					296428d53e | ||
| 
						 | 
					e0ca876de2 | ||
| 
						 | 
					a431a4fa04 | ||
| 
						 | 
					cc2bd03ec9 | ||
| 
						 | 
					1fe81b7d1e | ||
| 
						 | 
					0bd5a0d92d | ||
| 
						 | 
					330ddb6a30 | ||
| 
						 | 
					52dbd702ad | ||
| 
						 | 
					d7c3570ba3 | ||
| 
						 | 
					ab4d51b40b | ||
| 
						 | 
					1665c93d3b | ||
| 
						 | 
					b51fdbce9f | ||
| 
						 | 
					351b423e15 | ||
| 
						 | 
					7690be1647 | ||
| 
						 | 
					68aeb93afa | ||
| 
						 | 
					51062863a5 | ||
| 
						 | 
					4fb4b7aa6c | ||
| 
						 | 
					7f3cbcedc0 | ||
| 
						 | 
					6ef09def81 | ||
| 
						 | 
					c4c6aff9a5 | ||
| 
						 | 
					d71850cef6 | ||
| 
						 | 
					2597c9bfac | ||
| 
						 | 
					93307b57aa | ||
| 
						 | 
					618953c865 | ||
| 
						 | 
					e04dd78624 | ||
| 
						 | 
					fa0c4025f7 | ||
| 
						 | 
					2d2d185200 | ||
| 
						 | 
					cb7278eb50 | ||
| 
						 | 
					89aa114192 | ||
| 
						 | 
					ed062e0ce5 | ||
| 
						 | 
					a69ef8402b | ||
| 
						 | 
					8779f67d2d | ||
| 
						 | 
					e4b72136b8 | ||
| 
						 | 
					4ff5091bc2 | ||
| 
						 | 
					6f131250f1 | ||
| 
						 | 
					221a63d980 | ||
| 
						 | 
					d02eda147c | ||
| 
						 | 
					9b25716136 | ||
| 
						 | 
					6628a47f23 | ||
| 
						 | 
					ec0e6bc3f8 | ||
| 
						 | 
					d2c02be3a0 | ||
| 
						 | 
					594492fbdd | ||
| 
						 | 
					bd9ea7a88d | ||
| 
						 | 
					51327a4056 | ||
| 
						 | 
					33bd60528b | ||
| 
						 | 
					7e54474111 | ||
| 
						 | 
					e307069d62 | ||
| 
						 | 
					91db63294c | ||
| 
						 | 
					fd04e08c9c | ||
| 
						 | 
					6576409d60 | ||
| 
						 | 
					045cb2058c | ||
| 
						 | 
					d03afc12fd | ||
| 
						 | 
					48799a3cff | ||
| 
						 | 
					dba259e9f1 | ||
| 
						 | 
					07885f5810 | ||
| 
						 | 
					696c518550 | ||
| 
						 | 
					411ef2691c | ||
| 
						 | 
					fc6074ea9f | ||
| 
						 | 
					ab1670e2ce | ||
| 
						 | 
					9142a33bbf | ||
| 
						 | 
					f6eefa4ecc | ||
| 
						 | 
					f1db166ac4 | ||
| 
						 | 
					887c2bc56d | ||
| 
						 | 
					f0738a93c3 | ||
| 
						 | 
					75381c2c6e | ||
| 
						 | 
					bf0b9959d1 | ||
| 
						 | 
					406a54b597 | ||
| 
						 | 
					be04d1a862 | ||
| 
						 | 
					85b2d5a124 | ||
| 
						 | 
					521a7ed7b0 | ||
| 
						 | 
					529b188164 | ||
| 
						 | 
					8d307d8134 | ||
| 
						 | 
					8c675b52bc | ||
| 
						 | 
					aa51aa2aa0 | ||
| 
						 | 
					86865c6da5 | ||
| 
						 | 
					45296100df | ||
| 
						 | 
					1605fbc012 | ||
| 
						 | 
					c6c92e273d | ||
| 
						 | 
					467b373c43 | ||
| 
						 | 
					72ce7f06e9 | ||
| 
						 | 
					346a7284f7 | ||
| 
						 | 
					ee4ac67081 | ||
| 
						 | 
					5a93d14d75 | ||
| 
						 | 
					96a47a60ad | ||
| 
						 | 
					b24a47ad7f | ||
| 
						 | 
					cd1fd1bb7c | ||
| 
						 | 
					d44df7b6e6 | ||
| 
						 | 
					9d1ac0c84b | ||
| 
						 | 
					76af9cba5a | ||
| 
						 | 
					b69fc30902 | ||
| 
						 | 
					c3174f4de9 | ||
| 
						 | 
					99ce68e9ba | ||
| 
						 | 
					0cf73673a9 | ||
| 
						 | 
					08f442dc7b | ||
| 
						 | 
					8a8b95228c | ||
| 
						 | 
					31a752fa21 | ||
| 
						 | 
					a83831e68d | ||
| 
						 | 
					a12a8d4fe2 | ||
| 
						 | 
					e57f3a7e6c | ||
| 
						 | 
					68fbed9281 | ||
| 
						 | 
					8bfaa007d5 | ||
| 
						 | 
					76360f89c1 | ||
| 
						 | 
					d525230abd | ||
| 
						 | 
					b4aa637d41 | ||
| 
						 | 
					7c4334d0de | ||
| 
						 | 
					062be8d7c9 | ||
| 
						 | 
					db25ee59c5 | ||
| 
						 | 
					4b0bc6d0bf | ||
| 
						 | 
					8c0b04b995 | ||
| 
						 | 
					e5989adf92 | ||
| 
						 | 
					9e5da2f9d7 | ||
| 
						 | 
					a284a228a3 | ||
| 
						 | 
					2133e0d1be | ||
| 
						 | 
					a6f37f1d61 | ||
| 
						 | 
					9de9151826 | ||
| 
						 | 
					fdd5ada98c | ||
| 
						 | 
					80fcf18e24 | ||
| 
						 | 
					ab94b5ca7a | ||
| 
						 | 
					8d2ce56c37 | ||
| 
						 | 
					1ec324354b | ||
| 
						 | 
					16be6601c8 | ||
| 
						 | 
					98027446c8 | ||
| 
						 | 
					f2f1d874e1 | ||
| 
						 | 
					25a72113b1 | ||
| 
						 | 
					79c4ad5015 | ||
| 
						 | 
					e24f1c7c87 | ||
| 
						 | 
					dbf8a326d5 | ||
| 
						 | 
					0bc9c70c66 | ||
| 
						 | 
					594d2155e3 | ||
| 
						 | 
					20dbd71306 | ||
| 
						 | 
					6a727b9723 | ||
| 
						 | 
					02a5bc096f | ||
| 
						 | 
					2110db6f0c | ||
| 
						 | 
					2bac867382 | ||
| 
						 | 
					5fbd8a3be0 | ||
| 
						 | 
					ad6440b603 | ||
| 
						 | 
					064b6a915f | ||
| 
						 | 
					1578ebb0e2 | ||
| 
						 | 
					73525a4bbc | ||
| 
						 | 
					d62f49d1fc | ||
| 
						 | 
					63b88e77f2 | ||
| 
						 | 
					3d8f15c20b | ||
| 
						 | 
					cac5d56d60 | ||
| 
						 | 
					bd2a672c14 | ||
| 
						 | 
					82396e73f5 | ||
| 
						 | 
					ba928b169d | ||
| 
						 | 
					4fed720f97 | ||
| 
						 | 
					78238c85d4 | ||
| 
						 | 
					4f2ae7b73f | ||
| 
						 | 
					f82a9cc7ac | ||
| 
						 | 
					cce7624ab8 | ||
| 
						 | 
					c5ecd09172 | ||
| 
						 | 
					7b21c1c2f4 | ||
| 
						 | 
					f8714d81f5 | ||
| 
						 | 
					8622656005 | ||
| 
						 | 
					52237fadb6 | ||
| 
						 | 
					222cccf388 | ||
| 
						 | 
					bab308508e | ||
| 
						 | 
					dedb83c867 | ||
| 
						 | 
					723a90cdd6 | ||
| 
						 | 
					67d2398fa8 | ||
| 
						 | 
					5f3b6ec007 | ||
| 
						 | 
					55ab0c12f1 | ||
| 
						 | 
					d1227b5fc9 | ||
| 
						 | 
					6ea368c383 | ||
| 
						 | 
					e92b6de09f | ||
| 
						 | 
					e622587db4 | ||
| 
						 | 
					f2efc06d1f | ||
| 
						 | 
					a2b94452db | ||
| 
						 | 
					4c506f7cc3 | ||
| 
						 | 
					7886f05e88 | ||
| 
						 | 
					f58be0d1c1 | ||
| 
						 | 
					1152394bc1 | ||
| 
						 | 
					a082b5a590 | ||
| 
						 | 
					bae9484df2 | ||
| 
						 | 
					6f78485878 | ||
| 
						 | 
					fd0fe3390b | ||
| 
						 | 
					2522158127 | ||
| 
						 | 
					8be107cecc | ||
| 
						 | 
					5aab158c0b | ||
| 
						 | 
					1d33e60e36 | ||
| 
						 | 
					83c28cb857 | ||
| 
						 | 
					df5bce27b0 | ||
| 
						 | 
					2b15739b48 | ||
| 
						 | 
					3480c88e90 | ||
| 
						 | 
					432cd0f99d | ||
| 
						 | 
					e8b3e9b22d | ||
| 
						 | 
					d4a47671ea | ||
| 
						 | 
					0bcd1e62f3 | ||
| 
						 | 
					80822b7fff | ||
| 
						 | 
					78f1011f52 | ||
| 
						 | 
					67f6257617 | ||
| 
						 | 
					169c614489 | ||
| 
						 | 
					da908c438a | ||
| 
						 | 
					9c9c4bf1f9 | ||
| 
						 | 
					7764493298 | ||
| 
						 | 
					64a20ee61b | ||
| 
						 | 
					62d1af8c37 | ||
| 
						 | 
					0f5274fdf6 | ||
| 
						 | 
					2e2187ebf4 | ||
| 
						 | 
					762c3350f4 | ||
| 
						 | 
					e1a4d7f77e | ||
| 
						 | 
					a7a4554a85 | ||
| 
						 | 
					6bd808ce91 | ||
| 
						 | 
					a5c143bc46 | ||
| 
						 | 
					87c9cac756 | ||
| 
						 | 
					6a047f8722 | ||
| 
						 | 
					6523494e83 | ||
| 
						 | 
					7c6ce8bb90 | ||
| 
						 | 
					dafbfe4021 | ||
| 
						 | 
					a4d5c94d9b | ||
| 
						 | 
					7119e378a7 | ||
| 
						 | 
					e1dc3032c1 | ||
| 
						 | 
					5de03b8921 | ||
| 
						 | 
					7631d43c48 | ||
| 
						 | 
					d0b2ee5c85 | ||
| 
						 | 
					8830a5a1df | ||
| 
						 | 
					ee87626a93 | ||
| 
						 | 
					9f15d38c1c | ||
| 
						 | 
					4a96a977c0 | ||
| 
						 | 
					9a95293bdf | ||
| 
						 | 
					0b3a06d263 | ||
| 
						 | 
					9a6249c4f5 | ||
| 
						 | 
					50bd51e461 | ||
| 
						 | 
					04f8013314 | ||
| 
						 | 
					a0aaf0057a | ||
| 
						 | 
					8e78b3e6be | ||
| 
						 | 
					57a503818d | ||
| 
						 | 
					25d2ff3e9b | ||
| 
						 | 
					31902d3e57 | ||
| 
						 | 
					16f3fa6bae | ||
| 
						 | 
					1f706673cf | ||
| 
						 | 
					fac5f69ad2 | ||
| 
						 | 
					97c944bb63 | ||
| 
						 | 
					d0c4fe78ee | ||
| 
						 | 
					265457b451 | ||
| 
						 | 
					4a4a29c9f6 | ||
| 
						 | 
					0a91b9e1c9 | ||
| 
						 | 
					f56163295c | ||
| 
						 | 
					d1c87c068b | ||
| 
						 | 
					fa20761110 | ||
| 
						 | 
					e4a0e0a0e9 | ||
| 
						 | 
					d30ae19e2a | ||
| 
						 | 
					5c919e6bff | ||
| 
						 | 
					434393d1c3 | ||
| 
						 | 
					af9aa5d7cb | ||
| 
						 | 
					05eb75442a | ||
| 
						 | 
					3496ed0c7e | ||
| 
						 | 
					1b89604c7a | ||
| 
						 | 
					67a9d133e9 | ||
| 
						 | 
					ed9118b346 | ||
| 
						 | 
					59e55cfbd5 | ||
| 
						 | 
					788d3b32ac | ||
| 
						 | 
					1d414cf2fd | ||
| 
						 | 
					cc3c168162 | ||
| 
						 | 
					1ee6837f0e | ||
| 
						 | 
					27dcea7c5b | ||
| 
						 | 
					dcda7f7b8c | ||
| 
						 | 
					e0cbb69a4f | ||
| 
						 | 
					7ec95f786d | ||
| 
						 | 
					1efe40add5 | ||
| 
						 | 
					cbd73ee313 | ||
| 
						 | 
					34227a7a39 | ||
| 
						 | 
					71cb9b2d1d | ||
| 
						 | 
					cd4c9b194f | ||
| 
						 | 
					98762a0235 | ||
| 
						 | 
					2fd1fd9573 | ||
| 
						 | 
					aff3964078 | ||
| 
						 | 
					2778580397 | ||
| 
						 | 
					962062fe44 | ||
| 
						 | 
					0578b21270 | ||
| 
						 | 
					36a800c3f5 | ||
| 
						 | 
					6d21f84187 | ||
| 
						 | 
					f1e9833310 | ||
| 
						 | 
					46f5acc4f9 | ||
| 
						 | 
					95d4dcaeb3 | ||
| 
						 | 
					64c542e614 | ||
| 
						 | 
					13d081ea80 | ||
| 
						 | 
					c0f9d86287 | ||
| 
						 | 
					bcdecdaa73 | ||
| 
						 | 
					daac3ebca2 | ||
| 
						 | 
					639f9cf966 | ||
| 
						 | 
					4fc48b5aa4 | ||
| 
						 | 
					307ff77b42 | ||
| 
						 | 
					9b500bc5f7 | ||
| 
						 | 
					e313154134 | ||
| 
						 | 
					27e94c438d | ||
| 
						 | 
					58392876df | ||
| 
						 | 
					115c4b1aa7 | ||
| 
						 | 
					ba5649d259 | ||
| 
						 | 
					1b30575510 | ||
| 
						 | 
					7dbebd3ea7 | ||
| 
						 | 
					6f18790352 | ||
| 
						 | 
					d1e04a2ece | ||
| 
						 | 
					bea0bbd0c2 | ||
| 
						 | 
					0530503ef2 | ||
| 
						 | 
					d1e8ff814b | ||
| 
						 | 
					4f8ae761a2 | ||
| 
						 | 
					b530e92834 | ||
| 
						 | 
					b2a6777995 | ||
| 
						 | 
					b461fc5e40 | ||
| 
						 | 
					b7a8c6b60f | ||
| 
						 | 
					41aa8ad799 | ||
| 
						 | 
					7973baedd0 | ||
| 
						 | 
					299b71d982 | ||
| 
						 | 
					76aafe1fa8 | ||
| 
						 | 
					95a0229aaf | ||
| 
						 | 
					915a8fbad7 | ||
| 
						 | 
					d4d7fef313 | ||
| 
						 | 
					4e1dc9f885 | ||
| 
						 | 
					155ae80d22 | ||
| 
						 | 
					c7e336efd9 | ||
| 
						 | 
					ac3c65a0cc | ||
| 
						 | 
					df74df475b | ||
| 
						 | 
					a61e2db7cb | ||
| 
						 | 
					7aabe12acf | ||
| 
						 | 
					c4b75e5754 | ||
| 
						 | 
					6a7adb20a8 | ||
| 
						 | 
					b49fb2b69c | ||
| 
						 | 
					4bda29cb38 | ||
| 
						 | 
					5f14141ec9 | ||
| 
						 | 
					c088e45d85 | ||
| 
						 | 
					d59c51a94b | ||
| 
						 | 
					47b7fae61b | ||
| 
						 | 
					1a40b0c1e9 | ||
| 
						 | 
					27d886826c | ||
| 
						 | 
					18981cb636 | ||
| 
						 | 
					ffa8f65aa8 | ||
| 
						 | 
					82588b00c5 | ||
| 
						 | 
					603449e850 | ||
| 
						 | 
					248d88c849 | ||
| 
						 | 
					d19535fa21 | ||
| 
						 | 
					49204cafcc | ||
| 
						 | 
					812db2d267 | ||
| 
						 | 
					14490bea9f | ||
| 
						 | 
					0352970051 | ||
| 
						 | 
					ed01820722 | ||
| 
						 | 
					90a61f15cc | ||
| 
						 | 
					86cd7f1ba6 | ||
| 
						 | 
					d6ee55e35f | ||
| 
						 | 
					aef64eec32 | ||
| 
						 | 
					c4193d5ccd | ||
| 
						 | 
					0c94186818 | ||
| 
						 | 
					9039720013 | ||
| 
						 | 
					a3470f8aec | ||
| 
						 | 
					01badde21d | ||
| 
						 | 
					a37b232dd9 | ||
| 
						 | 
					579ee48385 | ||
| 
						 | 
					dd985d1dad | ||
| 
						 | 
					d2caea70a2 | ||
| 
						 | 
					21143cf5ee | ||
| 
						 | 
					dc2aed698d | ||
| 
						 | 
					37c350f19f | ||
| 
						 | 
					9e03fcf162 | ||
| 
						 | 
					8d4521c1df | ||
| 
						 | 
					9226252336 | ||
| 
						 | 
					f4fb83e787 | ||
| 
						 | 
					e7fcb25107 | ||
| 
						 | 
					5a85258f74 | ||
| 
						 | 
					2f7df2df43 | ||
| 
						 | 
					ad3a753718 | ||
| 
						 | 
					e45c551880 | ||
| 
						 | 
					e59d338d4e | ||
| 
						 | 
					7a86044f7a | ||
| 
						 | 
					8b98f605bc | ||
| 
						 | 
					7c773ebae0 | ||
| 
						 | 
					e84417430d | ||
| 
						 | 
					5a8d7b5f6d | ||
| 
						 | 
					cfb8107138 | ||
| 
						 | 
					43bd779fb7 | ||
| 
						 | 
					7f9a400776 | ||
| 
						 | 
					ce1c5873ac | ||
| 
						 | 
					85ff1995fd | ||
| 
						 | 
					b963f83c6a | ||
| 
						 | 
					f6297ebbb0 | ||
| 
						 | 
					a5259f56c5 | ||
| 
						 | 
					3f75ed9c18 | ||
| 
						 | 
					49ece51167 | ||
| 
						 | 
					e77c3eb20a | ||
| 
						 | 
					59b2a5f8d0 | ||
| 
						 | 
					28710d0bc7 | ||
| 
						 | 
					ad4d461606 | ||
| 
						 | 
					67905089ba | ||
| 
						 | 
					f2483af561 | ||
| 
						 | 
					c28b87641e | ||
| 
						 | 
					f8e6a69d6e | ||
| 
						 | 
					54216cec4b | ||
| 
						 | 
					12989bbd99 | ||
| 
						 | 
					38d09dba2e | ||
| 
						 | 
					fafd0c68e9 | ||
| 
						 | 
					41195c8e48 | ||
| 
						 | 
					a97804548e | ||
| 
						 | 
					ba653c0841 | ||
| 
						 | 
					5b191f78a0 | ||
| 
						 | 
					83ef61287e | ||
| 
						 | 
					3527e09bc5 | ||
| 
						 | 
					ddc5b3268f | ||
| 
						 | 
					22307b1934 | ||
| 
						 | 
					bd97357f8d | ||
| 
						 | 
					10dab1366e | ||
| 
						 | 
					52fc94c1fe | ||
| 
						 | 
					c1c7961dd6 | ||
| 
						 | 
					d3eef051b1 | ||
| 
						 | 
					57654df81e | ||
| 
						 | 
					0f791d7a9a | ||
| 
						 | 
					58779e0d65 | ||
| 
						 | 
					4ac361b5fd | ||
| 
						 | 
					1e2f27c061 | ||
| 
						 | 
					0302e4da82 | ||
| 
						 | 
					dc8743e0c0 | ||
| 
						 | 
					cc5ce3d5ae | ||
| 
						 | 
					caaf6f3012 | ||
| 
						 | 
					c5de8fd1cc | ||
| 
						 | 
					c9f23869e3 | ||
| 
						 | 
					61208c0e35 | ||
| 
						 | 
					dcffc74255 | ||
| 
						 | 
					23e23be1a6 | ||
| 
						 | 
					710427248a | ||
| 
						 | 
					a868042de2 | ||
| 
						 | 
					15296cd8b4 | ||
| 
						 | 
					717023245f | ||
| 
						 | 
					320be5bffa | ||
| 
						 | 
					778abea2d9 | ||
| 
						 | 
					835a1ac3a6 | ||
| 
						 | 
					20a7ef33f1 | ||
| 
						 | 
					e72612c7ff | ||
| 
						 | 
					04e0f001b0 | ||
| 
						 | 
					5db24aa901 | ||
| 
						 | 
					aec5e3d77b | ||
| 
						 | 
					335ddf8db5 | ||
| 
						 | 
					4abaf2b236 | ||
| 
						 | 
					183d212431 | ||
| 
						 | 
					e99532fb89 | ||
| 
						 | 
					4aa646f6b0 | ||
| 
						 | 
					9dcd51fb80 | ||
| 
						 | 
					6dee988b76 | ||
| 
						 | 
					5af40db396 | ||
| 
						 | 
					b3553bee7a | ||
| 
						 | 
					ac19c94b9f | ||
| 
						 | 
					845f7dc331 | ||
| 
						 | 
					2adeae37e1 | ||
| 
						 | 
					16eb12b2a0 | ||
| 
						 | 
					8411f2aa32 | ||
| 
						 | 
					e8acc49cbd | ||
| 
						 | 
					4bed073c65 | ||
| 
						 | 
					272735fb26 | ||
| 
						 | 
					b75cf2c189 | ||
| 
						 | 
					1aaa992250 | ||
| 
						 | 
					6256c066f1 | ||
| 
						 | 
					870b89a8f0 | ||
| 
						 | 
					65ac96913c | ||
| 
						 | 
					480945cb09 | ||
| 
						 | 
					bfc7130ed8 | ||
| 
						 | 
					a0938d9386 | ||
| 
						 | 
					2338c69d40 | ||
| 
						 | 
					c714501a0e | ||
| 
						 | 
					a58a3e5000 | ||
| 
						 | 
					ba35212b67 | ||
| 
						 | 
					f3e0358de7 | ||
| 
						 | 
					8064744d3a | ||
| 
						 | 
					d261949db2 | ||
| 
						 | 
					877f0fe2e8 | ||
| 
						 | 
					003d85772c | ||
| 
						 | 
					e7e10131de | ||
| 
						 | 
					830361e48b | ||
| 
						 | 
					1b1a9ce250 | ||
| 
						 | 
					25ac4c708f | ||
| 
						 | 
					c268e90f49 | ||
| 
						 | 
					c17512b7ab | ||
| 
						 | 
					1b837b3dc7 | ||
| 
						 | 
					2ece724f75 | ||
| 
						 | 
					276ac840aa | ||
| 
						 | 
					1f91461853 | ||
| 
						 | 
					1f9874102a | ||
| 
						 | 
					822605c157 | ||
| 
						 | 
					e49266ae43 | ||
| 
						 | 
					62e9de1a3b | ||
| 
						 | 
					2ddc4f7ae9 | ||
| 
						 | 
					2dd402675d | ||
| 
						 | 
					25b1af1e11 | ||
| 
						 | 
					75fb2b8156 | ||
| 
						 | 
					2a403f8b85 | ||
| 
						 | 
					c3d45a9f06 | ||
| 
						 | 
					c07b85b625 | ||
| 
						 | 
					511f653e6e | ||
| 
						 | 
					5636eaca6d | ||
| 
						 | 
					4b839b9958 | ||
| 
						 | 
					3f79da84d5 | ||
| 
						 | 
					d540638223 | ||
| 
						 | 
					4ec9b6dd4e | ||
| 
						 | 
					3bc219167a | ||
| 
						 | 
					8a55c97b4e | ||
| 
						 | 
					9e34162a09 | ||
| 
						 | 
					860a371eeb | ||
| 
						 | 
					41a46526a1 | ||
| 
						 | 
					46b798ac1b | ||
| 
						 | 
					359d0f2910 | ||
| 
						 | 
					ad3cb0386b | ||
| 
						 | 
					3a183cb218 | ||
| 
						 | 
					2eecaccd1c | ||
| 
						 | 
					5f30a98bc1 | ||
| 
						 | 
					b8a2fcbaff | ||
| 
						 | 
					01496cd080 | ||
| 
						 | 
					6a968ab82a | ||
| 
						 | 
					c0c4890887 | ||
| 
						 | 
					171a53592d | ||
| 
						 | 
					7811c330db | ||
| 
						 | 
					9bcd131e66 | ||
| 
						 | 
					c791423dd5 | ||
| 
						 | 
					80bdf38388 | ||
| 
						 | 
					9d9cb32f4e | ||
| 
						 | 
					87229bab13 | ||
| 
						 | 
					f065e9e4d5 | ||
| 
						 | 
					3812693111 | ||
| 
						 | 
					dd3c572256 | ||
| 
						 | 
					c5dfe40326 | ||
| 
						 | 
					ef278301e3 | ||
| 
						 | 
					2888fd64b0 | ||
| 
						 | 
					27c0f37e49 | ||
| 
						 | 
					0774f6a5e7 | ||
| 
						 | 
					4036d4459b | ||
| 
						 | 
					ee643de5b6 | ||
| 
						 | 
					8c7549a09e | ||
| 
						 | 
					7a16146304 | ||
| 
						 | 
					3d3809a21b | ||
| 
						 | 
					29465397dd | ||
| 
						 | 
					d300bb1735 | ||
| 
						 | 
					2e703472f1 | ||
| 
						 | 
					8fede90b9e | ||
| 
						 | 
					d128f157c4 | ||
| 
						 | 
					4fcedabfd0 | ||
| 
						 | 
					246c8e4f74 | ||
| 
						 | 
					4d2207aba7 | ||
| 
						 | 
					17b8b86d68 | ||
| 
						 | 
					fdb57230a3 | ||
| 
						 | 
					7469732bbc | ||
| 
						 | 
					d1dd6c3440 | ||
| 
						 | 
					02612c0061 | ||
| 
						 | 
					a4db63a773 | ||
| 
						 | 
					035c2b906a | ||
| 
						 | 
					6ea8be5749 | ||
| 
						 | 
					36024d5439 | ||
| 
						 | 
					8d52c98373 | ||
| 
						 | 
					b4a4eb0057 | ||
| 
						 | 
					b469c8ddbd | ||
| 
						 | 
					eee0036c7f | ||
| 
						 | 
					89c66b9430 | ||
| 
						 | 
					bd38319d83 | ||
| 
						 | 
					33dffd5ea8 | ||
| 
						 | 
					57176dadd4 | ||
| 
						 | 
					dd449a8705 | ||
| 
						 | 
					587ad9f41d | ||
| 
						 | 
					a16ad8bf3b | ||
| 
						 | 
					1e0490bd36 | ||
| 
						 | 
					8afc641f0c | ||
| 
						 | 
					2e4d58cb92 | ||
| 
						 | 
					02d7e2db65 | ||
| 
						 | 
					f935c573e9 | ||
| 
						 | 
					4a25e66c00 | ||
| 
						 | 
					95f4e3448e | ||
| 
						 | 
					eacb1c1771 | ||
| 
						 | 
					07fd825349 | ||
| 
						 | 
					be15cc8a36 | ||
| 
						 | 
					2f68519b3c | ||
| 
						 | 
					efe641f202 | ||
| 
						 | 
					9bd663046a | ||
| 
						 | 
					11b07f01ba | ||
| 
						 | 
					6c2f370e6b | ||
| 
						 | 
					936bccccd2 | ||
| 
						 | 
					c30ffeb81e | ||
| 
						 | 
					e05a323afd | ||
| 
						 | 
					80895deae2 | ||
| 
						 | 
					eddc691fc9 | ||
| 
						 | 
					deb2d7194d | ||
| 
						 | 
					fd8cfb11fb | ||
| 
						 | 
					9407aa4600 | ||
| 
						 | 
					263b8da37d | ||
| 
						 | 
					b95988b4e2 | ||
| 
						 | 
					35025e164a | ||
| 
						 | 
					32bbab8518 | ||
| 
						 | 
					84c0b745af | ||
| 
						 | 
					8b286fb009 | ||
| 
						 | 
					386fa58b67 | ||
| 
						 | 
					c5cfbc2297 | ||
| 
						 | 
					cd0a2beb11 | ||
| 
						 | 
					73f01ad8d8 | ||
| 
						 | 
					930b639cc9 | ||
| 
						 | 
					58483ea70c | ||
| 
						 | 
					072cac0347 | ||
| 
						 | 
					956d7cf3f3 | ||
| 
						 | 
					7558a2162e | ||
| 
						 | 
					62b165c0b4 | ||
| 
						 | 
					fe258e1b67 | ||
| 
						 | 
					dc37232100 | ||
| 
						 | 
					163f55f9c2 | ||
| 
						 | 
					2d16fd085e | ||
| 
						 | 
					e1a5f5bca5 | ||
| 
						 | 
					6e772ee189 | ||
| 
						 | 
					2b0f178ba3 | ||
| 
						 | 
					79e6c9fa6c | ||
| 
						 | 
					1426ddec5f | ||
| 
						 | 
					e9105003b0 | ||
| 
						 | 
					587bb06558 | ||
| 
						 | 
					53e9664cde | ||
| 
						 | 
					482fbac68f | ||
| 
						 | 
					dcccd43427 | ||
| 
						 | 
					397b8ff892 | ||
| 
						 | 
					38a4cf315a | ||
| 
						 | 
					5f8b24e32c | ||
| 
						 | 
					678a7ceb4e | ||
| 
						 | 
					077d494c7b | ||
| 
						 | 
					09b243d8c2 | ||
| 
						 | 
					991183e514 | ||
| 
						 | 
					9bf10e4b58 | ||
| 
						 | 
					884599d27d | ||
| 
						 | 
					f8a6e65bfd | ||
| 
						 | 
					6df6c5d615 | ||
| 
						 | 
					93114b7682 | ||
| 
						 | 
					9987ac3f13 | ||
| 
						 | 
					01a32b2154 | ||
| 
						 | 
					b3c3142bb2 | ||
| 
						 | 
					77f1a959c3 | ||
| 
						 | 
					e3dda0e812 | ||
| 
						 | 
					38103d36b4 | ||
| 
						 | 
					7685fe1724 | ||
| 
						 | 
					01afe03a3f | ||
| 
						 | 
					7fbbf89c58 | ||
| 
						 | 
					84d259d8b3 | ||
| 
						 | 
					8b47670a74 | ||
| 
						 | 
					7f5dc1d461 | ||
| 
						 | 
					43e765f4f9 | ||
| 
						 | 
					adec73f542 | ||
| 
						 | 
					fee159541f | ||
| 
						 | 
					d81e6bf6ce | ||
| 
						 | 
					70c93d970c | ||
| 
						 | 
					4960273832 | ||
| 
						 | 
					6c018ee6fe | ||
| 
						 | 
					4ef32103ca | ||
| 
						 | 
					e4ec27c5e2 | ||
| 
						 | 
					20c04f7977 | ||
| 
						 | 
					571f50d734 | ||
| 
						 | 
					780ea6f7c0 | ||
| 
						 | 
					4279906f6e | ||
| 
						 | 
					2e54b97fc2 | ||
| 
						 | 
					e1641b2c2e | ||
| 
						 | 
					e0e1e4be80 | ||
| 
						 | 
					d5845ce900 | ||
| 
						 | 
					85f2cde4c3 | ||
| 
						 | 
					cef64e01b3 | ||
| 
						 | 
					94ea775232 | ||
| 
						 | 
					2e4b7fac11 | ||
| 
						 | 
					2867ec459a | ||
| 
						 | 
					cd18d89894 | ||
| 
						 | 
					449ed31e25 | ||
| 
						 | 
					1f36904588 | ||
| 
						 | 
					f7495dd0c3 | ||
| 
						 | 
					a11f77835d | ||
| 
						 | 
					af1ad82c8e | ||
| 
						 | 
					4976338677 | ||
| 
						 | 
					99d130d1ed | ||
| 
						 | 
					4fb0544b0e | ||
| 
						 | 
					0b4ac61435 | ||
| 
						 | 
					1d5cd1d7c4 | ||
| 
						 | 
					08ebee6b4f | ||
| 
						 | 
					14830d9f1c | ||
| 
						 | 
					a3dd0f1345 | ||
| 
						 | 
					37873acfcd | ||
| 
						 | 
					2dbe0eb557 | ||
| 
						 | 
					50a0df4279 | ||
| 
						 | 
					c3a8b7a997 | ||
| 
						 | 
					95fac548bb | ||
| 
						 | 
					581847f415 | ||
| 
						 | 
					1b15897135 | ||
| 
						 | 
					8e606e3cef | ||
| 
						 | 
					be513622ac | ||
| 
						 | 
					6f309f2108 | ||
| 
						 | 
					92d9db5a2d | ||
| 
						 | 
					96620a3c2c | ||
| 
						 | 
					5249568b8e | ||
| 
						 | 
					4a336a6bba | ||
| 
						 | 
					60223d7f63 | ||
| 
						 | 
					5131253191 | ||
| 
						 | 
					035dc042a1 | ||
| 
						 | 
					dfc513530b | ||
| 
						 | 
					721e0a2dcd | ||
| 
						 | 
					8452eb12da | ||
| 
						 | 
					475bed5e19 | ||
| 
						 | 
					40a967523c | ||
| 
						 | 
					d3a34af073 | ||
| 
						 | 
					e7107cf782 | ||
| 
						 | 
					b7c918a195 | ||
| 
						 | 
					61e4c9b28c | ||
| 
						 | 
					e93847a95e | ||
| 
						 | 
					545377742c | ||
| 
						 | 
					47d38192b2 | ||
| 
						 | 
					ac80c47036 | ||
| 
						 | 
					1e84afbd90 | ||
| 
						 | 
					d31e641bac | ||
| 
						 | 
					4380c48b4b | ||
| 
						 | 
					db0e4ba8c5 | ||
| 
						 | 
					2d6ed51d94 | ||
| 
						 | 
					9ca4fe7a5e | ||
| 
						 | 
					e52b040b9c | ||
| 
						 | 
					1accee1653 | ||
| 
						 | 
					fff6f08cb6 | ||
| 
						 | 
					0e527a4252 | ||
| 
						 | 
					f10251a1a3 | ||
| 
						 | 
					0d4bad16a3 | ||
| 
						 | 
					8c6be434ac | ||
| 
						 | 
					3ca4309e8a | ||
| 
						 | 
					e8a2e1af63 | ||
| 
						 | 
					1d240140c9 | ||
| 
						 | 
					272eef544f | ||
| 
						 | 
					fd756c5332 | ||
| 
						 | 
					dce600ad51 | ||
| 
						 | 
					d02a737e0c | ||
| 
						 | 
					98ff59c716 | ||
| 
						 | 
					0e96e9f9be | ||
| 
						 | 
					e8c7898583 | ||
| 
						 | 
					11f4a6897a | ||
| 
						 | 
					002c5fd0d1 | ||
| 
						 | 
					18504ec08d | ||
| 
						 | 
					4737442185 | ||
| 
						 | 
					596096d6da | ||
| 
						 | 
					6af82401fc | ||
| 
						 | 
					a0b84beb9b | ||
| 
						 | 
					0816e96831 | ||
| 
						 | 
					7baf386ede | ||
| 
						 | 
					6e410b096e | ||
| 
						 | 
					f9e5994348 | ||
| 
						 | 
					ee77272cfd | ||
| 
						 | 
					16ed2aca6a | ||
| 
						 | 
					0f530e7902 | ||
| 
						 | 
					4ed66ce20e | ||
| 
						 | 
					b30e85836e | ||
| 
						 | 
					e449a97bd0 | ||
| 
						 | 
					39043f3fa4 | ||
| 
						 | 
					12389d602e | ||
| 
						 | 
					44144587a0 | ||
| 
						 | 
					d0a30e354b | ||
| 
						 | 
					c261dc89d5 | ||
| 
						 | 
					c2c135bca2 | ||
| 
						 | 
					eb20cb237d | ||
| 
						 | 
					106404d32f | ||
| 
						 | 
					e06efbad9f | ||
| 
						 | 
					3311c7f923 | ||
| 
						 | 
					3a6c655dfb | ||
| 
						 | 
					e11d786775 | ||
| 
						 | 
					889b6debc4 | ||
| 
						 | 
					9cb3413d9c | ||
| 
						 | 
					131826e1d1 | ||
| 
						 | 
					96e21dd051 | ||
| 
						 | 
					32e5f396e7 | ||
| 
						 | 
					6c6000dbbd | ||
| 
						 | 
					24defcb970 | ||
| 
						 | 
					a1a11a88b3 | ||
| 
						 | 
					a997ae29ad | ||
| 
						 | 
					ff94796700 | ||
| 
						 | 
					1f72ca4c4e | ||
| 
						 | 
					46faad8b57 | ||
| 
						 | 
					30f30364d5 | ||
| 
						 | 
					073d90da88 | ||
| 
						 | 
					c769e23a9a | ||
| 
						 | 
					9db48f4794 | ||
| 
						 | 
					911c597377 | ||
| 
						 | 
					28244ffd9a | ||
| 
						 | 
					3e38c7945c | ||
| 
						 | 
					79ffb76f6e | ||
| 
						 | 
					5fe4b749cf | ||
| 
						 | 
					6991d85da9 | ||
| 
						 | 
					c1c187a1ab | ||
| 
						 | 
					055d12e3ef | ||
| 
						 | 
					b49429d722 | ||
| 
						 | 
					815c7f8d64 | ||
| 
						 | 
					c879f79456 | ||
| 
						 | 
					3bc25f4707 | ||
| 
						 | 
					300cfe044a | ||
| 
						 | 
					fb586f4a96 | ||
| 
						 | 
					ced371bece | ||
| 
						 | 
					a87cac1982 | ||
| 
						 | 
					8fb5c7afa6 | ||
| 
						 | 
					aceb830378 | 
							
								
								
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					Dockerfile
 | 
				
			||||||
 | 
					tgs.Dockerfile
 | 
				
			||||||
							
								
								
									
										3
									
								
								.fixmie.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.fixmie.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					go:
 | 
				
			||||||
 | 
					  comments:
 | 
				
			||||||
 | 
					    disabled: true
 | 
				
			||||||
							
								
								
									
										36
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					<!-- This is a bug report template. By following the instructions below and
 | 
				
			||||||
 | 
					filling out the sections with your information, you will help the us to get all
 | 
				
			||||||
 | 
					the necessary data to fix your issue.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can also preview your report before submitting it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Text between <!-- and --> marks will be invisible in the report.
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue. -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Environment
 | 
				
			||||||
 | 
					<!-- run `matterbridge -version` -->
 | 
				
			||||||
 | 
					<!-- If you're having problems with mattermost also specify the mattermost version. -->
 | 
				
			||||||
 | 
					Version:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- What operating system are you using ? (be as specific as possible) -->
 | 
				
			||||||
 | 
					Operating system:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- If you compiled matterbridge yourself:
 | 
				
			||||||
 | 
					       * Specify the output of `go version` 
 | 
				
			||||||
 | 
					       * Specify the output of `git rev-parse HEAD` -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Please describe the expected behavior.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Please describe the actual behavior. 
 | 
				
			||||||
 | 
					<!-- Use logs from running `matterbridge -debug` if possible. -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Any steps to reproduce the behavior?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Please add your configuration file 
 | 
				
			||||||
 | 
					<!-- (be sure to exclude or anonymize private data (tokens/passwords)) -->
 | 
				
			||||||
							
								
								
									
										27
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: Bug report
 | 
				
			||||||
 | 
					about: Create a report to help us improve. (Check the FAQ on the wiki first)
 | 
				
			||||||
 | 
					labels: bug
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe the bug**
 | 
				
			||||||
 | 
					A clear and concise description of what the bug is.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**To Reproduce**
 | 
				
			||||||
 | 
					Steps to reproduce the behavior:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Expected behavior**
 | 
				
			||||||
 | 
					A clear and concise description of what you expected to happen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Screenshots/debug logs**
 | 
				
			||||||
 | 
					If applicable, add screenshots to help explain your problem.
 | 
				
			||||||
 | 
					Use logs from running `matterbridge -debug` if possible.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Environment (please complete the following information):**
 | 
				
			||||||
 | 
					 - OS: [e.g. linux]
 | 
				
			||||||
 | 
					 - Matterbridge version: output of  `matterbridge -version`
 | 
				
			||||||
 | 
					 - If self compiled: output of `git rev-parse HEAD`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Additional context**
 | 
				
			||||||
 | 
					Please add your configuration file  (be sure to exclude or anonymize private data (tokens/passwords))
 | 
				
			||||||
							
								
								
									
										18
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: Feature request
 | 
				
			||||||
 | 
					about: Suggest an idea for this project
 | 
				
			||||||
 | 
					labels: enhancement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Is your feature request related to a problem? Please describe.**
 | 
				
			||||||
 | 
					A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe the solution you'd like**
 | 
				
			||||||
 | 
					A clear and concise description of what you want to happen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe alternatives you've considered**
 | 
				
			||||||
 | 
					A clear and concise description of any alternative solutions or features you've considered.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Additional context**
 | 
				
			||||||
 | 
					Add any other context or screenshots about the feature request here.
 | 
				
			||||||
							
								
								
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					# For most projects, this workflow file will not need changing; you simply need
 | 
				
			||||||
 | 
					# to commit it to your repository.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# You may wish to alter this file to override the set of languages analyzed,
 | 
				
			||||||
 | 
					# or to provide custom queries or build logic.
 | 
				
			||||||
 | 
					name: "CodeQL"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [master]
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    # The branches below must be a subset of the branches above
 | 
				
			||||||
 | 
					    branches: [master]
 | 
				
			||||||
 | 
					  schedule:
 | 
				
			||||||
 | 
					    - cron: '0 16 * * 1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  analyze:
 | 
				
			||||||
 | 
					    name: Analyze
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        # Override automatic language detection by changing the below list
 | 
				
			||||||
 | 
					        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
 | 
				
			||||||
 | 
					        language: ['go']
 | 
				
			||||||
 | 
					        # Learn more...
 | 
				
			||||||
 | 
					        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - name: Checkout repository
 | 
				
			||||||
 | 
					      uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        # We must fetch at least the immediate parents so that if this is
 | 
				
			||||||
 | 
					        # a pull request then we can checkout the head.
 | 
				
			||||||
 | 
					        fetch-depth: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # If this run was triggered by a pull request event, then checkout
 | 
				
			||||||
 | 
					    # the head of the pull request instead of the merge commit.
 | 
				
			||||||
 | 
					    - run: git checkout HEAD^2
 | 
				
			||||||
 | 
					      if: ${{ github.event_name == 'pull_request' }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Initializes the CodeQL tools for scanning.
 | 
				
			||||||
 | 
					    - name: Initialize CodeQL
 | 
				
			||||||
 | 
					      uses: github/codeql-action/init@v1
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        languages: ${{ matrix.language }}
 | 
				
			||||||
 | 
					        # If you wish to specify custom queries, you can do so here or in a config file.
 | 
				
			||||||
 | 
					        # By default, queries listed here will override any specified in a config file. 
 | 
				
			||||||
 | 
					        # Prefix the list here with "+" to use these queries and those in the config file.
 | 
				
			||||||
 | 
					        # queries: ./path/to/local/query, your-org/your-repo/queries@main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
				
			||||||
 | 
					    # If this step fails, then you should remove it and run the build manually (see below)
 | 
				
			||||||
 | 
					    - name: Autobuild
 | 
				
			||||||
 | 
					      uses: github/codeql-action/autobuild@v1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ℹ️ Command-line programs to run using the OS shell.
 | 
				
			||||||
 | 
					    # 📚 https://git.io/JvXDl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
 | 
				
			||||||
 | 
					    #    and modify them (or add more) to build your code if your project
 | 
				
			||||||
 | 
					    #    uses a compiled language
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #- run: |
 | 
				
			||||||
 | 
					    #   make bootstrap
 | 
				
			||||||
 | 
					    #   make release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Perform CodeQL Analysis
 | 
				
			||||||
 | 
					      uses: github/codeql-action/analyze@v1
 | 
				
			||||||
							
								
								
									
										58
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					name: Development
 | 
				
			||||||
 | 
					on: [push, pull_request]
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  lint:
 | 
				
			||||||
 | 
					    name: golangci-lint
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          fetch-depth: 20
 | 
				
			||||||
 | 
					      - name: Run golangci-lint
 | 
				
			||||||
 | 
					        uses: golangci/golangci-lint-action@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          version: latest
 | 
				
			||||||
 | 
					          args: "-v --new-from-rev HEAD~5"
 | 
				
			||||||
 | 
					  test-build-upload:
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        go-version: [1.17.x]
 | 
				
			||||||
 | 
					        platform: [ubuntu-latest]
 | 
				
			||||||
 | 
					    runs-on: ${{ matrix.platform }}
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - name: Install Go
 | 
				
			||||||
 | 
					      uses: actions/setup-go@v2
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        go-version: ${{ matrix.go-version }}
 | 
				
			||||||
 | 
					        stable: false
 | 
				
			||||||
 | 
					    - name: Checkout code
 | 
				
			||||||
 | 
					      uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					          fetch-depth: 0
 | 
				
			||||||
 | 
					    - name: Test
 | 
				
			||||||
 | 
					      run: go test ./... -mod=vendor
 | 
				
			||||||
 | 
					    - name: Build
 | 
				
			||||||
 | 
					      run: |
 | 
				
			||||||
 | 
					        mkdir -p output/{win,lin,arm,mac}
 | 
				
			||||||
 | 
					        VERSION=$(git describe --tags)
 | 
				
			||||||
 | 
					        CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64
 | 
				
			||||||
 | 
					        CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
 | 
				
			||||||
 | 
					        CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64
 | 
				
			||||||
 | 
					    - name: Upload linux 64-bit
 | 
				
			||||||
 | 
					      if: startsWith(matrix.go-version,'1.17')
 | 
				
			||||||
 | 
					      uses: actions/upload-artifact@v2
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        name: matterbridge-linux-64bit
 | 
				
			||||||
 | 
					        path: output/lin
 | 
				
			||||||
 | 
					    - name: Upload windows 64-bit
 | 
				
			||||||
 | 
					      if: startsWith(matrix.go-version,'1.17')
 | 
				
			||||||
 | 
					      uses: actions/upload-artifact@v2
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        name: matterbridge-windows-64bit
 | 
				
			||||||
 | 
					        path: output/win
 | 
				
			||||||
 | 
					    - name: Upload darwin 64-bit
 | 
				
			||||||
 | 
					      if: startsWith(matrix.go-version,'1.17')
 | 
				
			||||||
 | 
					      uses: actions/upload-artifact@v2
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        name: matterbridge-darwin-64bit
 | 
				
			||||||
 | 
					        path: output/mac
 | 
				
			||||||
							
								
								
									
										68
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					name: docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - 'master'
 | 
				
			||||||
 | 
					    tags:
 | 
				
			||||||
 | 
					      - 'v*'
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - 'master'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  docker:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Checkout
 | 
				
			||||||
 | 
					        uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Set up QEMU
 | 
				
			||||||
 | 
					        uses: docker/setup-qemu-action@v1
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          platforms: amd64,arm64
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Set up Docker Buildx
 | 
				
			||||||
 | 
					        uses: docker/setup-buildx-action@v1
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Docker meta
 | 
				
			||||||
 | 
					        id: meta
 | 
				
			||||||
 | 
					        uses: docker/metadata-action@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          images: 42wim/matterbridge,ghcr.io/42wim/matterbridge
 | 
				
			||||||
 | 
					          flavor: |
 | 
				
			||||||
 | 
					            latest=true
 | 
				
			||||||
 | 
					          tags: |
 | 
				
			||||||
 | 
					            type=ref,event=branch
 | 
				
			||||||
 | 
					            type=ref,event=pr
 | 
				
			||||||
 | 
					            type=semver,pattern={{version}}
 | 
				
			||||||
 | 
					            type=semver,pattern=stable
 | 
				
			||||||
 | 
					            type=semver,pattern={{major}}
 | 
				
			||||||
 | 
					            type=semver,pattern={{major}}.{{minor}}
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Login to DockerHub
 | 
				
			||||||
 | 
					        uses: docker/login-action@v1 
 | 
				
			||||||
 | 
					        if: github.event_name != 'pull_request'
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
				
			||||||
 | 
					          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Log into registry ghcr.io
 | 
				
			||||||
 | 
					        uses: docker/login-action@v1
 | 
				
			||||||
 | 
					        if: github.event_name != 'pull_request'
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          registry: ghcr.io
 | 
				
			||||||
 | 
					          username: ${{ github.repository_owner }}
 | 
				
			||||||
 | 
					          password: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Build and push
 | 
				
			||||||
 | 
					        id: docker_build
 | 
				
			||||||
 | 
					        uses: docker/build-push-action@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          context: .
 | 
				
			||||||
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
 | 
					          push: ${{ github.event_name != 'pull_request' }}
 | 
				
			||||||
 | 
					          tags: ${{ steps.meta.outputs.tags }}
 | 
				
			||||||
 | 
					          labels: ${{ steps.meta.outputs.labels }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					# Exclude matterbridge binary
 | 
				
			||||||
 | 
					/matterbridge
 | 
				
			||||||
 | 
					/matterbridge.exe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Exclude configuration file
 | 
				
			||||||
 | 
					matterbridge.toml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Exclude IDE Files
 | 
				
			||||||
 | 
					.vscode
 | 
				
			||||||
							
								
								
									
										236
									
								
								.golangci.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								.golangci.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,236 @@
 | 
				
			|||||||
 | 
					# For full documentation of the configuration options please
 | 
				
			||||||
 | 
					# see: https://github.com/golangci/golangci-lint#config-file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# options for analysis running
 | 
				
			||||||
 | 
					run:
 | 
				
			||||||
 | 
					  # default concurrency is the available CPU number
 | 
				
			||||||
 | 
					  # concurrency: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # timeout for analysis, e.g. 30s, 5m, default is 1m
 | 
				
			||||||
 | 
					  deadline: 5m
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # exit code when at least one issue was found, default is 1
 | 
				
			||||||
 | 
					  issues-exit-code: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # include test files or not, default is true
 | 
				
			||||||
 | 
					  tests: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # list of build tags, all linters use it. Default is empty list.
 | 
				
			||||||
 | 
					  build-tags:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # which dirs to skip: they won't be analyzed;
 | 
				
			||||||
 | 
					  # can use regexp here: generated.*, regexp is applied on full path;
 | 
				
			||||||
 | 
					  # default value is empty list, but next dirs are always skipped independently
 | 
				
			||||||
 | 
					  # from this option's value:
 | 
				
			||||||
 | 
					  #   	vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
 | 
				
			||||||
 | 
					  skip-dirs: gateway/bridgemap$
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # which files to skip: they will be analyzed, but issues from them
 | 
				
			||||||
 | 
					  # won't be reported. Default value is empty list, but there is
 | 
				
			||||||
 | 
					  # no need to include all autogenerated files, we confidently recognize
 | 
				
			||||||
 | 
					  # autogenerated files. If it's not please let us know.
 | 
				
			||||||
 | 
					  skip-files:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# output configuration options
 | 
				
			||||||
 | 
					output:
 | 
				
			||||||
 | 
					  # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
 | 
				
			||||||
 | 
					  format: colored-line-number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # print lines of code with issue, default is true
 | 
				
			||||||
 | 
					  print-issued-lines: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # print linter name in the end of issue text, default is true
 | 
				
			||||||
 | 
					  print-linter-name: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# all available settings of specific linters, we can set an option for
 | 
				
			||||||
 | 
					# a given linter even if we deactivate that same linter at runtime
 | 
				
			||||||
 | 
					linters-settings:
 | 
				
			||||||
 | 
					  errcheck:
 | 
				
			||||||
 | 
					    # report about not checking of errors in type assertions: `a := b.(MyStruct)`;
 | 
				
			||||||
 | 
					    # default is false: such cases aren't reported by default.
 | 
				
			||||||
 | 
					    check-type-assertions: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
 | 
				
			||||||
 | 
					    # default is false: such cases aren't reported by default.
 | 
				
			||||||
 | 
					    check-blank: false
 | 
				
			||||||
 | 
					  govet:
 | 
				
			||||||
 | 
					    # report about shadowed variables
 | 
				
			||||||
 | 
					    check-shadowing: true
 | 
				
			||||||
 | 
					  golint:
 | 
				
			||||||
 | 
					    # minimal confidence for issues, default is 0.8
 | 
				
			||||||
 | 
					    min-confidence: 0.8
 | 
				
			||||||
 | 
					  gofmt:
 | 
				
			||||||
 | 
					    # simplify code: gofmt with `-s` option, true by default
 | 
				
			||||||
 | 
					    simplify: true
 | 
				
			||||||
 | 
					  goimports:
 | 
				
			||||||
 | 
					    # put imports beginning with prefix after 3rd-party packages;
 | 
				
			||||||
 | 
					    # it's a comma-separated list of prefixes
 | 
				
			||||||
 | 
					    local-prefixes: github.com
 | 
				
			||||||
 | 
					  gocyclo:
 | 
				
			||||||
 | 
					    # minimal code complexity to report, 30 by default (but we recommend 10-20)
 | 
				
			||||||
 | 
					    min-complexity: 15
 | 
				
			||||||
 | 
					  maligned:
 | 
				
			||||||
 | 
					    # print struct with more effective memory layout or not, false by default
 | 
				
			||||||
 | 
					    suggest-new: true
 | 
				
			||||||
 | 
					  dupl:
 | 
				
			||||||
 | 
					    # tokens count to trigger issue, 150 by default
 | 
				
			||||||
 | 
					    threshold: 150
 | 
				
			||||||
 | 
					  goconst:
 | 
				
			||||||
 | 
					    # minimal length of string constant, 3 by default
 | 
				
			||||||
 | 
					    min-len: 3
 | 
				
			||||||
 | 
					    # minimal occurrences count to trigger, 3 by default
 | 
				
			||||||
 | 
					    min-occurrences: 3
 | 
				
			||||||
 | 
					  depguard:
 | 
				
			||||||
 | 
					    list-type: blacklist
 | 
				
			||||||
 | 
					    include-go-root: false
 | 
				
			||||||
 | 
					    packages:
 | 
				
			||||||
 | 
					    # List of packages that we would want to blacklist for... reasons.
 | 
				
			||||||
 | 
					  misspell:
 | 
				
			||||||
 | 
					    # Correct spellings using locale preferences for US or UK.
 | 
				
			||||||
 | 
					    # Default is to use a neutral variety of English.
 | 
				
			||||||
 | 
					    # Setting locale to US will correct the British spelling of 'colour' to 'color'.
 | 
				
			||||||
 | 
					  lll:
 | 
				
			||||||
 | 
					    # max line length, lines longer will be reported. Default is 120.
 | 
				
			||||||
 | 
					    # '\t' is counted as 1 character by default, and can be changed with the tab-width option
 | 
				
			||||||
 | 
					    line-length: 150
 | 
				
			||||||
 | 
					    # tab width in spaces. Default to 1.
 | 
				
			||||||
 | 
					    tab-width: 1
 | 
				
			||||||
 | 
					  unused:
 | 
				
			||||||
 | 
					    # treat code as a program (not a library) and report unused exported identifiers; default is false.
 | 
				
			||||||
 | 
					    # XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
 | 
				
			||||||
 | 
					    # if it's called for subdir of a project it can't find funcs usages. All text editor integrations
 | 
				
			||||||
 | 
					    # with golangci-lint call it on a directory with the changed file.
 | 
				
			||||||
 | 
					    check-exported: false
 | 
				
			||||||
 | 
					  unparam:
 | 
				
			||||||
 | 
					    # Inspect exported functions, default is false. Set to true if no external program/library imports your code.
 | 
				
			||||||
 | 
					    # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
 | 
				
			||||||
 | 
					    # if it's called for subdir of a project it can't find external interfaces. All text editor integrations
 | 
				
			||||||
 | 
					    # with golangci-lint call it on a directory with the changed file.
 | 
				
			||||||
 | 
					    check-exported: false
 | 
				
			||||||
 | 
					  nakedret:
 | 
				
			||||||
 | 
					    # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
 | 
				
			||||||
 | 
					    max-func-lines: 0 # Warn on all naked returns.
 | 
				
			||||||
 | 
					  prealloc:
 | 
				
			||||||
 | 
					    # XXX: we don't recommend using this linter before doing performance profiling.
 | 
				
			||||||
 | 
					    # For most programs usage of prealloc will be a premature optimization.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
 | 
				
			||||||
 | 
					    # True by default.
 | 
				
			||||||
 | 
					    simple: true
 | 
				
			||||||
 | 
					    range-loops: true # Report preallocation suggestions on range loops, true by default
 | 
				
			||||||
 | 
					    for-loops: false # Report preallocation suggestions on for loops, false by default
 | 
				
			||||||
 | 
					  gocritic:
 | 
				
			||||||
 | 
					    # which checks should be enabled; can't be combined with 'disabled-checks';
 | 
				
			||||||
 | 
					    # default are: [appendAssign assignOp caseOrder dupArg dupBranchBody dupCase flagDeref
 | 
				
			||||||
 | 
					    # ifElseChain regexpMust singleCaseSwitch sloppyLen switchTrue typeSwitchVar underef
 | 
				
			||||||
 | 
					    # unlambda unslice rangeValCopy defaultCaseOrder];
 | 
				
			||||||
 | 
					    # all checks list: https://github.com/go-critic/checkers
 | 
				
			||||||
 | 
					    # disabled for now - hugeParam
 | 
				
			||||||
 | 
					    enabled-checks:
 | 
				
			||||||
 | 
					      - appendAssign
 | 
				
			||||||
 | 
					      - assignOp
 | 
				
			||||||
 | 
					      - boolExprSimplify
 | 
				
			||||||
 | 
					      - builtinShadow
 | 
				
			||||||
 | 
					      - captLocal
 | 
				
			||||||
 | 
					      - caseOrder
 | 
				
			||||||
 | 
					      - commentedOutImport
 | 
				
			||||||
 | 
					      - defaultCaseOrder
 | 
				
			||||||
 | 
					      - dupArg
 | 
				
			||||||
 | 
					      - dupBranchBody
 | 
				
			||||||
 | 
					      - dupCase
 | 
				
			||||||
 | 
					      - dupSubExpr
 | 
				
			||||||
 | 
					      - elseif
 | 
				
			||||||
 | 
					      - emptyFallthrough
 | 
				
			||||||
 | 
					      - ifElseChain
 | 
				
			||||||
 | 
					      - importShadow
 | 
				
			||||||
 | 
					      - indexAlloc
 | 
				
			||||||
 | 
					      - methodExprCall
 | 
				
			||||||
 | 
					      - nestingReduce
 | 
				
			||||||
 | 
					      - offBy1
 | 
				
			||||||
 | 
					      - ptrToRefParam
 | 
				
			||||||
 | 
					      - regexpMust
 | 
				
			||||||
 | 
					      - singleCaseSwitch
 | 
				
			||||||
 | 
					      - sloppyLen
 | 
				
			||||||
 | 
					      - switchTrue
 | 
				
			||||||
 | 
					      - typeSwitchVar
 | 
				
			||||||
 | 
					      - typeUnparen
 | 
				
			||||||
 | 
					      - underef
 | 
				
			||||||
 | 
					      - unlambda
 | 
				
			||||||
 | 
					      - unnecessaryBlock
 | 
				
			||||||
 | 
					      - unslice
 | 
				
			||||||
 | 
					      - valSwap
 | 
				
			||||||
 | 
					      - wrapperFunc
 | 
				
			||||||
 | 
					      - yodaStyleExpr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# linters that we should / shouldn't run
 | 
				
			||||||
 | 
					linters:
 | 
				
			||||||
 | 
					  enable-all: true
 | 
				
			||||||
 | 
					  disable:
 | 
				
			||||||
 | 
					    - gochecknoglobals
 | 
				
			||||||
 | 
					    - lll
 | 
				
			||||||
 | 
					    - maligned
 | 
				
			||||||
 | 
					    - prealloc
 | 
				
			||||||
 | 
					    - wsl
 | 
				
			||||||
 | 
					    - gomnd
 | 
				
			||||||
 | 
					    - godox
 | 
				
			||||||
 | 
					    - goerr113
 | 
				
			||||||
 | 
					    - testpackage
 | 
				
			||||||
 | 
					    - godot
 | 
				
			||||||
 | 
					    - interfacer
 | 
				
			||||||
 | 
					    - goheader
 | 
				
			||||||
 | 
					    - noctx
 | 
				
			||||||
 | 
					    - gci
 | 
				
			||||||
 | 
					    - errorlint
 | 
				
			||||||
 | 
					    - nlreturn
 | 
				
			||||||
 | 
					    - exhaustivestruct
 | 
				
			||||||
 | 
					    - forbidigo
 | 
				
			||||||
 | 
					    - wrapcheck
 | 
				
			||||||
 | 
					    - varnamelen
 | 
				
			||||||
 | 
					    - ireturn
 | 
				
			||||||
 | 
					    - errorlint
 | 
				
			||||||
 | 
					    - tparallel
 | 
				
			||||||
 | 
					    - wrapcheck
 | 
				
			||||||
 | 
					    - paralleltest
 | 
				
			||||||
 | 
					    - makezero
 | 
				
			||||||
 | 
					    - thelper
 | 
				
			||||||
 | 
					    - cyclop
 | 
				
			||||||
 | 
					    - revive
 | 
				
			||||||
 | 
					    - importas
 | 
				
			||||||
 | 
					    - gomoddirectives
 | 
				
			||||||
 | 
					    - promlinter
 | 
				
			||||||
 | 
					    - tagliatelle
 | 
				
			||||||
 | 
					    - errname
 | 
				
			||||||
 | 
					    - typecheck
 | 
				
			||||||
 | 
					# rules to deal with reported isues
 | 
				
			||||||
 | 
					issues:
 | 
				
			||||||
 | 
					  # List of regexps of issue texts to exclude, empty list by default.
 | 
				
			||||||
 | 
					  # But independently from this option we use default exclude patterns,
 | 
				
			||||||
 | 
					  # it can be disabled by `exclude-use-default: false`. To list all
 | 
				
			||||||
 | 
					  # excluded by default patterns execute `golangci-lint run --help`
 | 
				
			||||||
 | 
					  exclude:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Independently from option `exclude` we use default exclude patterns,
 | 
				
			||||||
 | 
					  # it can be disabled by this option. To list all
 | 
				
			||||||
 | 
					  # excluded by default patterns execute `golangci-lint run --help`.
 | 
				
			||||||
 | 
					  # Default value for this option is true.
 | 
				
			||||||
 | 
					  exclude-use-default: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
 | 
				
			||||||
 | 
					  max-per-linter: 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
 | 
				
			||||||
 | 
					  max-same-issues: 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Show only new issues: if there are unstaged changes or untracked files,
 | 
				
			||||||
 | 
					  # only those changes are analyzed, else only changes in HEAD~ are analyzed.
 | 
				
			||||||
 | 
					  # It's a super-useful option for integration of golangci-lint into existing
 | 
				
			||||||
 | 
					  # large codebase. It's not practical to fix all existing issues at the moment
 | 
				
			||||||
 | 
					  # of integration: much better don't allow issues in new code.
 | 
				
			||||||
 | 
					  # Default is false.
 | 
				
			||||||
 | 
					  new: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Show only new issues created after git revision `REV`
 | 
				
			||||||
 | 
					  new-from-rev: "HEAD~1"
 | 
				
			||||||
							
								
								
									
										41
									
								
								.goreleaser.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								.goreleaser.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					release:
 | 
				
			||||||
 | 
					  prerelease: auto
 | 
				
			||||||
 | 
					  name_template: "{{.ProjectName}} v{{.Version}}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					builds:
 | 
				
			||||||
 | 
					- env:
 | 
				
			||||||
 | 
					    - CGO_ENABLED=0
 | 
				
			||||||
 | 
					  goos:
 | 
				
			||||||
 | 
					    - freebsd
 | 
				
			||||||
 | 
					    - windows
 | 
				
			||||||
 | 
					    - darwin
 | 
				
			||||||
 | 
					    - linux
 | 
				
			||||||
 | 
					    - dragonfly
 | 
				
			||||||
 | 
					    - netbsd
 | 
				
			||||||
 | 
					    - openbsd
 | 
				
			||||||
 | 
					  goarch:
 | 
				
			||||||
 | 
					    - amd64
 | 
				
			||||||
 | 
					    - arm
 | 
				
			||||||
 | 
					    - arm64
 | 
				
			||||||
 | 
					    - 386
 | 
				
			||||||
 | 
					  goarm:
 | 
				
			||||||
 | 
					    - 6
 | 
				
			||||||
 | 
					    - 7
 | 
				
			||||||
 | 
					  ldflags:
 | 
				
			||||||
 | 
					    - -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					archives:
 | 
				
			||||||
 | 
					  -
 | 
				
			||||||
 | 
					    id: matterbridge
 | 
				
			||||||
 | 
					    builds:
 | 
				
			||||||
 | 
					    - matterbridge
 | 
				
			||||||
 | 
					    name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
 | 
				
			||||||
 | 
					    format: binary
 | 
				
			||||||
 | 
					    files:
 | 
				
			||||||
 | 
					      - none*
 | 
				
			||||||
 | 
					    replacements:
 | 
				
			||||||
 | 
					      386: 32bit
 | 
				
			||||||
 | 
					      amd64: 64bit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					checksum:
 | 
				
			||||||
 | 
					  name_template: 'checksums.txt'
 | 
				
			||||||
							
								
								
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,11 +1,14 @@
 | 
				
			|||||||
FROM alpine:edge
 | 
					FROM alpine AS builder
 | 
				
			||||||
ENTRYPOINT ["/bin/matterbridge"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY . /go/src/github.com/42wim/matterbridge
 | 
					COPY . /go/src/matterbridge
 | 
				
			||||||
RUN apk update && apk add go git \
 | 
					RUN apk --no-cache add go git \
 | 
				
			||||||
        && cd /go/src/github.com/42wim/matterbridge \
 | 
					        && cd /go/src/matterbridge \
 | 
				
			||||||
        && export GOPATH=/go \
 | 
					        && CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
 | 
				
			||||||
        && go get \
 | 
					
 | 
				
			||||||
        && go build -o /bin/matterbridge \
 | 
					FROM alpine
 | 
				
			||||||
        && rm -rf /go \
 | 
					RUN apk --no-cache add ca-certificates mailcap
 | 
				
			||||||
        && apk del --purge git go
 | 
					COPY --from=builder /bin/matterbridge /bin/matterbridge
 | 
				
			||||||
 | 
					RUN mkdir /etc/matterbridge \
 | 
				
			||||||
 | 
					  && touch /etc/matterbridge/matterbridge.toml \
 | 
				
			||||||
 | 
					  && ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
 | 
				
			||||||
 | 
					ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								Dockerfile_whatsappmulti
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Dockerfile_whatsappmulti
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					FROM alpine AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY . /go/src/matterbridge
 | 
				
			||||||
 | 
					RUN apk --no-cache add go git \
 | 
				
			||||||
 | 
					        && cd /go/src/matterbridge \
 | 
				
			||||||
 | 
					        && CGO_ENABLED=0 go build -tags whatsappmulti -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FROM alpine
 | 
				
			||||||
 | 
					RUN apk --no-cache add ca-certificates mailcap
 | 
				
			||||||
 | 
					COPY --from=builder /bin/matterbridge /bin/matterbridge
 | 
				
			||||||
 | 
					RUN mkdir /etc/matterbridge \
 | 
				
			||||||
 | 
					  && touch /etc/matterbridge/matterbridge.toml \
 | 
				
			||||||
 | 
					  && ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
 | 
				
			||||||
 | 
					ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]
 | 
				
			||||||
							
								
								
									
										499
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										499
									
								
								README.md
									
									
									
									
									
								
							@@ -1,116 +1,441 @@
 | 
				
			|||||||
 | 
					<div align="center">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# matterbridge
 | 
					# matterbridge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Simple bridge between mattermost and IRC. Uses the in/outgoing webhooks.  
 | 
					<br />
 | 
				
			||||||
Relays public channel messages between mattermost and IRC.  
 | 
					**A simple chat bridge**<br />
 | 
				
			||||||
 | 
					Letting people be where they want to be.<br />
 | 
				
			||||||
 | 
					<sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Requires mattermost 1.2.0+
 | 
					   <sup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
There is also [matterbridge-plus] (https://github.com/42wim/matterbridge-plus) which uses the mattermost API and needs a dedicated user (bot). But requires no incoming/outgoing webhook setup. 
 | 
					[Discord][mb-discord] |
 | 
				
			||||||
 | 
					[Gitter][mb-gitter] |
 | 
				
			||||||
 | 
					[IRC][mb-irc] |
 | 
				
			||||||
 | 
					[Keybase][mb-keybase] |
 | 
				
			||||||
 | 
					[Matrix][mb-matrix] |
 | 
				
			||||||
 | 
					[Mattermost][mb-mattermost] |
 | 
				
			||||||
 | 
					[MSTeams][mb-msteams] |
 | 
				
			||||||
 | 
					[Rocket.Chat][mb-rocketchat] |
 | 
				
			||||||
 | 
					[Slack][mb-slack] |
 | 
				
			||||||
 | 
					[Telegram][mb-telegram] |
 | 
				
			||||||
 | 
					[Twitch][mb-twitch] |
 | 
				
			||||||
 | 
					[WhatsApp][mb-whatsapp] |
 | 
				
			||||||
 | 
					[XMPP][mb-xmpp] |
 | 
				
			||||||
 | 
					[Zulip][mb-zulip] |
 | 
				
			||||||
 | 
					And more...
 | 
				
			||||||
 | 
					</sup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## binaries
 | 
					---
 | 
				
			||||||
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/tag/v0.4.2)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## building
 | 
					[](https://github.com/42wim/matterbridge/releases/latest)
 | 
				
			||||||
Go 1.6+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
 | 
					[](https://codeclimate.com/github/42wim/matterbridge/maintainability)
 | 
				
			||||||
 | 
					[](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					  <hr />
 | 
				
			||||||
cd $GOPATH
 | 
					</div>
 | 
				
			||||||
go get github.com/42wim/matterbridge
 | 
					<div align="right"><sup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Note:** Matter<em>most</em> isn't required to run matter<em>bridge</em>.</sup></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
					  <a href="https://www.digitalocean.com/">
 | 
				
			||||||
 | 
					    <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="201px">
 | 
				
			||||||
 | 
					  </a>
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Table of Contents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [matterbridge](#matterbridge)
 | 
				
			||||||
 | 
					- [Table of Contents](#table-of-contents)
 | 
				
			||||||
 | 
					  - [Features](#features)
 | 
				
			||||||
 | 
					    - [Natively supported](#natively-supported)
 | 
				
			||||||
 | 
					    - [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
 | 
				
			||||||
 | 
					    - [API](#api)
 | 
				
			||||||
 | 
					  - [Chat with us](#chat-with-us)
 | 
				
			||||||
 | 
					  - [Screenshots](#screenshots)
 | 
				
			||||||
 | 
					  - [Installing / upgrading](#installing--upgrading)
 | 
				
			||||||
 | 
					    - [Binaries](#binaries)
 | 
				
			||||||
 | 
					    - [Packages](#packages)
 | 
				
			||||||
 | 
					  - [Building](#building)
 | 
				
			||||||
 | 
					  - [Building with whatsapp (beta) multidevice support](#building-with-whatsapp-beta-multidevice-support)
 | 
				
			||||||
 | 
					  - [Configuration](#configuration)
 | 
				
			||||||
 | 
					    - [Basic configuration](#basic-configuration)
 | 
				
			||||||
 | 
					    - [Settings](#settings)
 | 
				
			||||||
 | 
					    - [Advanced configuration](#advanced-configuration)
 | 
				
			||||||
 | 
					      - [Examples](#examples)
 | 
				
			||||||
 | 
					        - [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
 | 
				
			||||||
 | 
					        - [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
 | 
				
			||||||
 | 
					    - [Running](#running)
 | 
				
			||||||
 | 
					      - [Docker](#docker)
 | 
				
			||||||
 | 
					      - [Systemd](#systemd)
 | 
				
			||||||
 | 
					    - [Changelog](#changelog)
 | 
				
			||||||
 | 
					    - [FAQ](#faq)
 | 
				
			||||||
 | 
					    - [Related projects](#related-projects)
 | 
				
			||||||
 | 
					    - [Articles / Tutorials](#articles--tutorials)
 | 
				
			||||||
 | 
					    - [Thanks](#thanks)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
 | 
				
			||||||
 | 
					- [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
 | 
				
			||||||
 | 
					- [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
 | 
				
			||||||
 | 
					- Preserves threading when possible
 | 
				
			||||||
 | 
					- [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
 | 
				
			||||||
 | 
					- [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
 | 
				
			||||||
 | 
					- [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
 | 
				
			||||||
 | 
					- [API](https://github.com/42wim/matterbridge/wiki/Features#api)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Natively supported
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Discord](https://discordapp.com)
 | 
				
			||||||
 | 
					- [Gitter](https://gitter.im)
 | 
				
			||||||
 | 
					- [Harmony](https://harmonyapp.io)
 | 
				
			||||||
 | 
					- [IRC](http://www.mirc.com/servers.html)
 | 
				
			||||||
 | 
					- [Keybase](https://keybase.io)
 | 
				
			||||||
 | 
					- [Matrix](https://matrix.org)
 | 
				
			||||||
 | 
					- [Mattermost](https://github.com/mattermost/mattermost-server/)
 | 
				
			||||||
 | 
					- [Microsoft Teams](https://teams.microsoft.com)
 | 
				
			||||||
 | 
					- [Mumble](https://www.mumble.info/)
 | 
				
			||||||
 | 
					- [Nextcloud Talk](https://nextcloud.com/talk/)
 | 
				
			||||||
 | 
					- [Rocket.chat](https://rocket.chat)
 | 
				
			||||||
 | 
					- [Slack](https://slack.com)
 | 
				
			||||||
 | 
					- [Ssh-chat](https://github.com/shazow/ssh-chat)
 | 
				
			||||||
 | 
					- ~~[Steam](https://store.steampowered.com/)~~
 | 
				
			||||||
 | 
					  - Not supported anymore, see [here](https://github.com/Philipp15b/go-steam/issues/94) for more info.
 | 
				
			||||||
 | 
					- [Telegram](https://telegram.org)
 | 
				
			||||||
 | 
					- [Twitch](https://twitch.tv)
 | 
				
			||||||
 | 
					- [VK](https://vk.com/)
 | 
				
			||||||
 | 
					- [WhatsApp](https://www.whatsapp.com/)
 | 
				
			||||||
 | 
					  - Whatsapp legacy is natively supported
 | 
				
			||||||
 | 
					  - Whatsapp multidevice beta is natively supported but you need to build yourself, see [here](#building-with-whatsapp-beta-multidevice-support)
 | 
				
			||||||
 | 
					- [XMPP](https://xmpp.org)
 | 
				
			||||||
 | 
					- [Zulip](https://zulipchat.com)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 3rd party via matterbridge api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Discourse](https://github.com/DeclanHoare/matterbabble)
 | 
				
			||||||
 | 
					- [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio)
 | 
				
			||||||
 | 
					- [Facebook messenger](https://github.com/VictorNine/fbridge)
 | 
				
			||||||
 | 
					- [Minecraft](https://github.com/elytra/MatterLink)
 | 
				
			||||||
 | 
					- [Minecraft](https://github.com/raws/mattercraft)
 | 
				
			||||||
 | 
					- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
 | 
				
			||||||
 | 
					- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
 | 
				
			||||||
 | 
					- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
 | 
				
			||||||
 | 
					- [MatterAMXX](https://github.com/GabeIggy/MatterAMXX)
 | 
				
			||||||
 | 
					- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
 | 
				
			||||||
 | 
					- [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge)
 | 
				
			||||||
 | 
					- [Teamspeak](https://github.com/Archeb/ts-matterbridge)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### API
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The API is basic at the moment.
 | 
				
			||||||
 | 
					More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Used by the projects below. Feel free to make a PR to add your project to this list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Forge server chat, archived)
 | 
				
			||||||
 | 
					- [MatterCraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Forge server chat)
 | 
				
			||||||
 | 
					- [MatterBukkit](https://gitlab.com/Programie/MatterBukkit) (Matterbridge link for Minecraft Bukkit/Spigot server chat)
 | 
				
			||||||
 | 
					- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
 | 
				
			||||||
 | 
					- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
 | 
				
			||||||
 | 
					- [fbridge-asyncio](https://github.com/powerjungle/fbridge-asyncio) (Facebook messenger support)
 | 
				
			||||||
 | 
					- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
 | 
				
			||||||
 | 
					- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
 | 
				
			||||||
 | 
					- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
 | 
				
			||||||
 | 
					- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
 | 
				
			||||||
 | 
					- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
 | 
				
			||||||
 | 
					- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Chat with us
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Questions or want to test on your favorite platform? Join below:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Discord][mb-discord]
 | 
				
			||||||
 | 
					- [Gitter][mb-gitter]
 | 
				
			||||||
 | 
					- [IRC][mb-irc]
 | 
				
			||||||
 | 
					- [Keybase][mb-keybase]
 | 
				
			||||||
 | 
					- [Matrix][mb-matrix]
 | 
				
			||||||
 | 
					- [Mattermost][mb-mattermost]
 | 
				
			||||||
 | 
					- [Rocket.Chat][mb-rocketchat]
 | 
				
			||||||
 | 
					- [Slack][mb-slack]
 | 
				
			||||||
 | 
					- [Telegram][mb-telegram]
 | 
				
			||||||
 | 
					- [Twitch][mb-twitch]
 | 
				
			||||||
 | 
					- [XMPP][mb-xmpp] (matterbridge@conference.jabber.de)
 | 
				
			||||||
 | 
					- [Zulip][mb-zulip]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See <https://github.com/42wim/matterbridge/wiki>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Installing / upgrading
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Binaries
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Latest stable release [v1.25.2](https://github.com/42wim/matterbridge/releases/latest)
 | 
				
			||||||
 | 
					- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). On \*nix platforms you may need to make the binary executable - you can do this by running `chmod a+x` on the binary (example: `chmod a+x matterbridge-1.24.1-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Packages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Overview](https://repology.org/metapackage/matterbridge/versions)
 | 
				
			||||||
 | 
					- [snap](https://snapcraft.io/matterbridge)
 | 
				
			||||||
 | 
					- [scoop](https://github.com/42wim/scoop-bucket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Building
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you really want to build from source, follow these instructions:
 | 
				
			||||||
 | 
					Go 1.17+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Building the binary with **all** the bridges enabled needs about 3GB RAM to compile.
 | 
				
			||||||
 | 
					You can reduce this memory requirement to 0,5GB RAM by adding the `nomsteams` tag if you don't need/use the Microsoft Teams bridge.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To install the latest stable run:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					go install github.com/42wim/matterbridge
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You should now have matterbridge binary in the bin directory:
 | 
					To install the latest dev run:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					go install github.com/42wim/matterbridge@master
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
$ ls bin/
 | 
					
 | 
				
			||||||
 | 
					To install the latest stable run without msteams or zulip bridge:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					go install -tags nomsteams,nozulip github.com/42wim/matterbridge
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You should now have matterbridge binary in the ~/go/bin directory:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					$ ls ~/go/bin/
 | 
				
			||||||
matterbridge
 | 
					matterbridge
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## running
 | 
					## Building with whatsapp (beta) multidevice support
 | 
				
			||||||
1) Copy the matterbridge.conf.sample to matterbridge.conf in the same directory as the matterbridge binary.  
 | 
					 | 
				
			||||||
2) Edit matterbridge.conf with the settings for your environment. See below for more config information.  
 | 
					 | 
				
			||||||
3) Now you can run matterbridge. 
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					Because the library we use for Whatsapp multidevice support includes a GPL3 library we can not provide you binaries.
 | 
				
			||||||
Usage of matterbridge:
 | 
					(as this would require the Matterbridge to change it license to GPL)
 | 
				
			||||||
  -conf="matterbridge.conf": config file
 | 
					
 | 
				
			||||||
 | 
					Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					So this means you have to build it yourself using the instructions below:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					go install -tags whatsappmulti github.com/42wim/matterbridge@master
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Matterbridge will:
 | 
					If you're low on memory and don't need msteams:
 | 
				
			||||||
* start a webserver listening on the port specified in the configuration.
 | 
					 | 
				
			||||||
* connect to specified irc server and channel.
 | 
					 | 
				
			||||||
* send messages from mattermost to irc and vice versa, messages in mattermost will appear with irc-nick
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## config
 | 
					 | 
				
			||||||
### matterbridge
 | 
					 | 
				
			||||||
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Look at matterbridge.conf.sample for an example
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
[IRC]
 | 
					
 | 
				
			||||||
server="irc.freenode.net"
 | 
					You should now have matterbridge binary in the ~/go/bin directory:
 | 
				
			||||||
port=6667
 | 
					
 | 
				
			||||||
UseTLS=false
 | 
					```bash
 | 
				
			||||||
SkipTLSVerify=true
 | 
					$ ls ~/go/bin/
 | 
				
			||||||
nick="matterbot"
 | 
					matterbridge
 | 
				
			||||||
channel="#matterbridge"
 | 
					```
 | 
				
			||||||
UseSlackCircumfix=false
 | 
					
 | 
				
			||||||
#Freenode nickserv
 | 
					## Configuration
 | 
				
			||||||
NickServNick="nickserv"
 | 
					
 | 
				
			||||||
#Password for nickserv
 | 
					### Basic configuration
 | 
				
			||||||
NickServPassword="secret"
 | 
					
 | 
				
			||||||
#Ignore the messages from these nicks. They will not be sent to mattermost
 | 
					See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
 | 
				
			||||||
IgnoreNicks="ircspammer1 ircspammer2"
 | 
					
 | 
				
			||||||
 | 
					### Settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Advanced configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Bridge mattermost (off-topic) - irc (#testing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```toml
 | 
				
			||||||
 | 
					[irc]
 | 
				
			||||||
 | 
					    [irc.libera]
 | 
				
			||||||
 | 
					    Server="irc.libera.chat:6667"
 | 
				
			||||||
 | 
					    Nick="yourbotname"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[mattermost]
 | 
					[mattermost]
 | 
				
			||||||
#url is your incoming webhook url (account settings - integrations - incoming webhooks)
 | 
					    [mattermost.work]
 | 
				
			||||||
url="http://mattermost.yourdomain.com/hooks/incomingwebhookkey"  
 | 
					    Server="yourmattermostserver.tld"
 | 
				
			||||||
#port the bridge webserver will listen on
 | 
					    Team="yourteam"
 | 
				
			||||||
port=9999
 | 
					    Login="yourlogin"
 | 
				
			||||||
#address the webserver will bind to
 | 
					    Password="yourpass"
 | 
				
			||||||
BindAddress="0.0.0.0"
 | 
					    PrefixMessagesWithNick=true
 | 
				
			||||||
showjoinpart=true #show irc users joining and parting
 | 
					    RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
				
			||||||
#the token you get from the outgoing webhook in mattermost. If empty no token check will be done.
 | 
					 | 
				
			||||||
#if you use multiple IRC channel (see below, this must be empty!)
 | 
					 | 
				
			||||||
token=yourtokenfrommattermost
 | 
					 | 
				
			||||||
#disable certificate checking (selfsigned certificates)
 | 
					 | 
				
			||||||
#SkipTLSVerify=true
 | 
					 | 
				
			||||||
#whether to prefix messages from IRC to mattermost with the sender's nick. Useful if username overrides for incoming webhooks isn't enabled on the mattermost server
 | 
					 | 
				
			||||||
PrefixMessagesWithNick=false
 | 
					 | 
				
			||||||
#how to format the list of IRC nicks when displayed in mattermost. Possible options are "table" and "plain"
 | 
					 | 
				
			||||||
NickFormatter=plain
 | 
					 | 
				
			||||||
#how many nicks to list per row for formatters that support this
 | 
					 | 
				
			||||||
NicksPerRow=4
 | 
					 | 
				
			||||||
#Ignore the messages from these nicks. They will not be sent to irc
 | 
					 | 
				
			||||||
IgnoreNicks="mmbot spammer2"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#multiple channel config
 | 
					[[gateway]]
 | 
				
			||||||
#token you can find in your outgoing webhook
 | 
					name="mygateway"
 | 
				
			||||||
[Token "outgoingwebhooktoken1"] 
 | 
					enable=true
 | 
				
			||||||
IRCChannel="#off-topic"
 | 
					    [[gateway.inout]]
 | 
				
			||||||
MMChannel="off-topic"
 | 
					    account="irc.libera"
 | 
				
			||||||
 | 
					    channel="#testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Token "outgoingwebhooktoken2"]
 | 
					    [[gateway.inout]]
 | 
				
			||||||
IRCChannel="#testing"
 | 
					    account="mattermost.work"
 | 
				
			||||||
MMChannel="testing"
 | 
					    channel="off-topic"
 | 
				
			||||||
 | 
					 | 
				
			||||||
[general]
 | 
					 | 
				
			||||||
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key
 | 
					 | 
				
			||||||
GiphyApiKey="dc6zaTOxFJmzC"
 | 
					 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### mattermost
 | 
					#### Bridge slack (#general) - discord (general)
 | 
				
			||||||
You'll have to configure the incoming en outgoing webhooks. 
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
* incoming webhooks
 | 
					```toml
 | 
				
			||||||
Go to "account settings" - integrations - "incoming webhooks".  
 | 
					[slack]
 | 
				
			||||||
Choose a channel at "Add a new incoming webhook", this will create a webhook URL right below.  
 | 
					[slack.test]
 | 
				
			||||||
This URL should be set in the matterbridge.conf in the [mattermost] section (see above)  
 | 
					Token="yourslacktoken"
 | 
				
			||||||
 | 
					PrefixMessagesWithNick=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* outgoing webhooks
 | 
					[discord]
 | 
				
			||||||
Go to "account settings" - integrations - "outgoing webhooks".  
 | 
					[discord.test]
 | 
				
			||||||
Choose a channel (the same as the one from incoming webhooks) and fill in the address and port of the server matterbridge will run on.  
 | 
					Token="yourdiscordtoken"
 | 
				
			||||||
 | 
					Server="yourdiscordservername"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
e.g. http://192.168.1.1:9999 (9999 is the port specified in [mattermost] section of matterbridge.conf)
 | 
					[general]
 | 
				
			||||||
 | 
					RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[gateway]]
 | 
				
			||||||
 | 
					    name = "mygateway"
 | 
				
			||||||
 | 
					    enable=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [[gateway.inout]]
 | 
				
			||||||
 | 
					    account = "discord.test"
 | 
				
			||||||
 | 
					    channel="general"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [[gateway.inout]]
 | 
				
			||||||
 | 
					    account ="slack.test"
 | 
				
			||||||
 | 
					    channel = "general"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Running
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					Usage of ./matterbridge:
 | 
				
			||||||
 | 
					  -conf string
 | 
				
			||||||
 | 
					        config file (default "matterbridge.toml")
 | 
				
			||||||
 | 
					  -debug
 | 
				
			||||||
 | 
					        enable debug
 | 
				
			||||||
 | 
					  -gops
 | 
				
			||||||
 | 
					        enable gops agent
 | 
				
			||||||
 | 
					  -version
 | 
				
			||||||
 | 
					        show version
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Systemd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Please take a look at the [Service Files page](https://github.com/42wim/matterbridge/wiki/Service-files) for more information.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## FAQ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Related projects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [jwflory/ansible-role-matterbridge](https://galaxy.ansible.com/jwflory/matterbridge) (Ansible role to simplify deploying Matterbridge)
 | 
				
			||||||
 | 
					- [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
 | 
				
			||||||
 | 
					- [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
 | 
				
			||||||
 | 
					- [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
 | 
				
			||||||
 | 
					- [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
 | 
				
			||||||
 | 
					- [matterlink](https://github.com/elytra/MatterLink)
 | 
				
			||||||
 | 
					- [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
 | 
				
			||||||
 | 
					- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
 | 
				
			||||||
 | 
					- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
 | 
				
			||||||
 | 
					- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
 | 
				
			||||||
 | 
					- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
 | 
				
			||||||
 | 
					- [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk)
 | 
				
			||||||
 | 
					- [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge)
 | 
				
			||||||
 | 
					- [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge)
 | 
				
			||||||
 | 
					- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
 | 
				
			||||||
 | 
					- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Articles / Tutorials
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
 | 
				
			||||||
 | 
					- <https://mattermost.com/blog/connect-irc-to-mattermost/>
 | 
				
			||||||
 | 
					- <https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/>
 | 
				
			||||||
 | 
					- <https://blog.brightscout.com/top-10-mattermost-integrations/>
 | 
				
			||||||
 | 
					- <https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/>
 | 
				
			||||||
 | 
					- <https://kopano.com/blog/matterbridge-bridging-mattermost-chat/>
 | 
				
			||||||
 | 
					- <https://www.stitcher.com/s/?eid=52382713>
 | 
				
			||||||
 | 
					- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
 | 
				
			||||||
 | 
					- <https://userlinux.net/mattermost-and-matterbridge.html>
 | 
				
			||||||
 | 
					- <https://nextcloud.com/blog/bridging-chat-services-in-talk/>
 | 
				
			||||||
 | 
					- <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/>
 | 
				
			||||||
 | 
					- Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Thanks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>This project is supported by:</p>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
					  <a href="https://www.digitalocean.com/">
 | 
				
			||||||
 | 
					    <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
 | 
				
			||||||
 | 
					  </a>
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Matterbridge wouldn't exist without these libraries:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- discord - <https://github.com/bwmarrin/discordgo>
 | 
				
			||||||
 | 
					- echo - <https://github.com/labstack/echo>
 | 
				
			||||||
 | 
					- gitter - <https://github.com/sromku/go-gitter>
 | 
				
			||||||
 | 
					- gops - <https://github.com/google/gops>
 | 
				
			||||||
 | 
					- gozulipbot - <https://github.com/ifo/gozulipbot>
 | 
				
			||||||
 | 
					- gumble - <https://github.com/layeh/gumble>
 | 
				
			||||||
 | 
					- harmony - <https://github.com/harmony-development/shibshib>
 | 
				
			||||||
 | 
					- irc - <https://github.com/lrstanley/girc>
 | 
				
			||||||
 | 
					- keybase - <https://github.com/keybase/go-keybase-chat-bot>
 | 
				
			||||||
 | 
					- matrix - <https://github.com/matrix-org/gomatrix>
 | 
				
			||||||
 | 
					- mattermost - <https://github.com/mattermost/mattermost-server>
 | 
				
			||||||
 | 
					- msgraph.go - <https://github.com/yaegashi/msgraph.go>
 | 
				
			||||||
 | 
					- mumble - <https://github.com/layeh/gumble>
 | 
				
			||||||
 | 
					- nctalk - <https://github.com/gary-kim/go-nc-talk>
 | 
				
			||||||
 | 
					- rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK>
 | 
				
			||||||
 | 
					- slack - <https://github.com/nlopes/slack>
 | 
				
			||||||
 | 
					- sshchat - <https://github.com/shazow/ssh-chat>
 | 
				
			||||||
 | 
					- steam - <https://github.com/Philipp15b/go-steam>
 | 
				
			||||||
 | 
					- telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api>
 | 
				
			||||||
 | 
					- tengo - <https://github.com/d5/tengo>
 | 
				
			||||||
 | 
					- vk - <https://github.com/SevereCloud/vksdk>
 | 
				
			||||||
 | 
					- whatsapp - <https://github.com/Rhymen/go-whatsapp>
 | 
				
			||||||
 | 
					- whatsapp - <https://github.com/tulir/whatsmeow>
 | 
				
			||||||
 | 
					- xmpp - <https://github.com/mattn/go-xmpp>
 | 
				
			||||||
 | 
					- zulip - <https://github.com/ifo/gozulipbot>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- Links -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[mb-discord]: https://discord.gg/AkKPtrQ
 | 
				
			||||||
 | 
					[mb-gitter]: https://gitter.im/42wim/matterbridge
 | 
				
			||||||
 | 
					[mb-irc]: https://web.libera.chat/#matterbridge
 | 
				
			||||||
 | 
					[mb-keybase]: https://keybase.io/team/matterbridge
 | 
				
			||||||
 | 
					[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
 | 
				
			||||||
 | 
					[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
 | 
				
			||||||
 | 
					[mb-msteams]: https://teams.microsoft.com/join/hj92x75gd3y7
 | 
				
			||||||
 | 
					[mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
 | 
				
			||||||
 | 
					[mb-slack]: https://join.slack.com/t/matterbridgechat/shared_invite/zt-2ourq2h2-7YvyYBq2WFGC~~zEzA68_Q
 | 
				
			||||||
 | 
					[mb-telegram]: https://t.me/Matterbridge
 | 
				
			||||||
 | 
					[mb-twitch]: https://www.twitch.tv/matterbridge
 | 
				
			||||||
 | 
					[mb-whatsapp]: https://www.whatsapp.com/
 | 
				
			||||||
 | 
					[mb-xmpp]: https://inverse.chat/
 | 
				
			||||||
 | 
					[mb-zulip]: https://matterbridge.zulipchat.com/register/
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										207
									
								
								bridge/api/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								bridge/api/api.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
				
			|||||||
 | 
					package api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gopkg.in/olahol/melody.v1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/labstack/echo/v4"
 | 
				
			||||||
 | 
						"github.com/labstack/echo/v4/middleware"
 | 
				
			||||||
 | 
						ring "github.com/zfjagann/golang-ring"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type API struct {
 | 
				
			||||||
 | 
						Messages ring.Ring
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
						mrouter *melody.Melody
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Message struct {
 | 
				
			||||||
 | 
						Text     string `json:"text"`
 | 
				
			||||||
 | 
						Username string `json:"username"`
 | 
				
			||||||
 | 
						UserID   string `json:"userid"`
 | 
				
			||||||
 | 
						Avatar   string `json:"avatar"`
 | 
				
			||||||
 | 
						Gateway  string `json:"gateway"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &API{Config: cfg}
 | 
				
			||||||
 | 
						e := echo.New()
 | 
				
			||||||
 | 
						e.HideBanner = true
 | 
				
			||||||
 | 
						e.HidePort = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.mrouter = melody.New()
 | 
				
			||||||
 | 
						b.mrouter.HandleMessage(func(s *melody.Session, msg []byte) {
 | 
				
			||||||
 | 
							message := config.Message{}
 | 
				
			||||||
 | 
							err := json.Unmarshal(msg, &message)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("failed to decode message from byte[] '%s'", string(msg))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.handleWebsocketMessage(message, s)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						b.mrouter.HandleConnect(func(session *melody.Session) {
 | 
				
			||||||
 | 
							greet := b.getGreeting()
 | 
				
			||||||
 | 
							data, err := json.Marshal(greet)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("failed to encode message '%v'", greet)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = session.Write(data)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("failed to write message '%s'", string(data))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// TODO: send message history buffer from `b.Messages` here
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Messages = ring.Ring{}
 | 
				
			||||||
 | 
						if b.GetInt("Buffer") != 0 {
 | 
				
			||||||
 | 
							b.Messages.SetCapacity(b.GetInt("Buffer"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString("Token") != "" {
 | 
				
			||||||
 | 
							e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
 | 
				
			||||||
 | 
								return key == b.GetString("Token"), nil
 | 
				
			||||||
 | 
							}))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set RemoteNickFormat to a sane default
 | 
				
			||||||
 | 
						if !b.IsKeySet("RemoteNickFormat") {
 | 
				
			||||||
 | 
							b.Log.Debugln("RemoteNickFormat is unset, defaulting to \"{NICK}\"")
 | 
				
			||||||
 | 
							b.Config.Config.Viper().Set(b.GetConfigKey("RemoteNickFormat"), "{NICK}")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						e.GET("/api/health", b.handleHealthcheck)
 | 
				
			||||||
 | 
						e.GET("/api/messages", b.handleMessages)
 | 
				
			||||||
 | 
						e.GET("/api/stream", b.handleStream)
 | 
				
			||||||
 | 
						e.GET("/api/websocket", b.handleWebsocket)
 | 
				
			||||||
 | 
						e.POST("/api/message", b.handlePostMessage)
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							if b.GetString("BindAddress") == "" {
 | 
				
			||||||
 | 
								b.Log.Fatalf("No BindAddress configured.")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Log.Infof("Listening on %s", b.GetString("BindAddress"))
 | 
				
			||||||
 | 
							b.Log.Fatal(e.Start(b.GetString("BindAddress")))
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) Connect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Lock()
 | 
				
			||||||
 | 
						defer b.Unlock()
 | 
				
			||||||
 | 
						// ignore delete messages
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("enqueueing message from %s on ring buffer", msg.Username)
 | 
				
			||||||
 | 
						b.Messages.Enqueue(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := json.Marshal(msg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("failed to encode message  '%s'", msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_ = b.mrouter.Broadcast(data)
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) handleHealthcheck(c echo.Context) error {
 | 
				
			||||||
 | 
						return c.String(http.StatusOK, "OK")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) handlePostMessage(c echo.Context) error {
 | 
				
			||||||
 | 
						message := config.Message{}
 | 
				
			||||||
 | 
						if err := c.Bind(&message); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// these values are fixed
 | 
				
			||||||
 | 
						message.Channel = "api"
 | 
				
			||||||
 | 
						message.Protocol = "api"
 | 
				
			||||||
 | 
						message.Account = b.Account
 | 
				
			||||||
 | 
						message.ID = ""
 | 
				
			||||||
 | 
						message.Timestamp = time.Now()
 | 
				
			||||||
 | 
						b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
 | 
				
			||||||
 | 
						b.Remote <- message
 | 
				
			||||||
 | 
						return c.JSON(http.StatusOK, message)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) handleMessages(c echo.Context) error {
 | 
				
			||||||
 | 
						b.Lock()
 | 
				
			||||||
 | 
						defer b.Unlock()
 | 
				
			||||||
 | 
						c.JSONPretty(http.StatusOK, b.Messages.Values(), " ")
 | 
				
			||||||
 | 
						b.Messages = ring.Ring{}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) getGreeting() config.Message {
 | 
				
			||||||
 | 
						return config.Message{
 | 
				
			||||||
 | 
							Event:     config.EventAPIConnected,
 | 
				
			||||||
 | 
							Timestamp: time.Now(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) handleStream(c echo.Context) error {
 | 
				
			||||||
 | 
						c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
 | 
				
			||||||
 | 
						c.Response().WriteHeader(http.StatusOK)
 | 
				
			||||||
 | 
						greet := b.getGreeting()
 | 
				
			||||||
 | 
						if err := json.NewEncoder(c.Response()).Encode(greet); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Response().Flush()
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							// TODO: this causes issues, messages should be broadcasted to all connected clients
 | 
				
			||||||
 | 
							msg := b.Messages.Dequeue()
 | 
				
			||||||
 | 
							if msg != nil {
 | 
				
			||||||
 | 
								if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								c.Response().Flush()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							time.Sleep(200 * time.Millisecond)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) handleWebsocketMessage(message config.Message, s *melody.Session) {
 | 
				
			||||||
 | 
						message.Channel = "api"
 | 
				
			||||||
 | 
						message.Protocol = "api"
 | 
				
			||||||
 | 
						message.Account = b.Account
 | 
				
			||||||
 | 
						message.ID = ""
 | 
				
			||||||
 | 
						message.Timestamp = time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := json.Marshal(message)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("failed to encode message for loopback '%v'", message)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_ = b.mrouter.BroadcastOthers(data, s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Sending websocket message from %s on %s to gateway", message.Username, "api")
 | 
				
			||||||
 | 
						b.Remote <- message
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *API) handleWebsocket(c echo.Context) error {
 | 
				
			||||||
 | 
						err := b.mrouter.HandleRequest(c.Response(), c.Request())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("error in websocket handling  '%v'", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										135
									
								
								bridge/bridge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								bridge/bridge.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					package bridge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bridger interface {
 | 
				
			||||||
 | 
						Send(msg config.Message) (string, error)
 | 
				
			||||||
 | 
						Connect() error
 | 
				
			||||||
 | 
						JoinChannel(channel config.ChannelInfo) error
 | 
				
			||||||
 | 
						Disconnect() error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bridge struct {
 | 
				
			||||||
 | 
						Bridger
 | 
				
			||||||
 | 
						*sync.RWMutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Name           string
 | 
				
			||||||
 | 
						Account        string
 | 
				
			||||||
 | 
						Protocol       string
 | 
				
			||||||
 | 
						Channels       map[string]config.ChannelInfo
 | 
				
			||||||
 | 
						Joined         map[string]bool
 | 
				
			||||||
 | 
						ChannelMembers *config.ChannelMembers
 | 
				
			||||||
 | 
						Log            *logrus.Entry
 | 
				
			||||||
 | 
						Config         config.Config
 | 
				
			||||||
 | 
						General        *config.Protocol
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Config struct {
 | 
				
			||||||
 | 
						*Bridge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Remote chan config.Message
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Factory is the factory function to create a bridge
 | 
				
			||||||
 | 
					type Factory func(*Config) Bridger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(bridge *config.Bridge) *Bridge {
 | 
				
			||||||
 | 
						accInfo := strings.Split(bridge.Account, ".")
 | 
				
			||||||
 | 
						if len(accInfo) != 2 {
 | 
				
			||||||
 | 
							log.Fatalf("config failure, account incorrect: %s", bridge.Account)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						protocol := accInfo[0]
 | 
				
			||||||
 | 
						name := accInfo[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &Bridge{
 | 
				
			||||||
 | 
							RWMutex:  new(sync.RWMutex),
 | 
				
			||||||
 | 
							Channels: make(map[string]config.ChannelInfo),
 | 
				
			||||||
 | 
							Name:     name,
 | 
				
			||||||
 | 
							Protocol: protocol,
 | 
				
			||||||
 | 
							Account:  bridge.Account,
 | 
				
			||||||
 | 
							Joined:   make(map[string]bool),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) JoinChannels() error {
 | 
				
			||||||
 | 
						return b.joinChannels(b.Channels, b.Joined)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetChannelMembers sets the newMembers to the bridge ChannelMembers
 | 
				
			||||||
 | 
					func (b *Bridge) SetChannelMembers(newMembers *config.ChannelMembers) {
 | 
				
			||||||
 | 
						b.Lock()
 | 
				
			||||||
 | 
						b.ChannelMembers = newMembers
 | 
				
			||||||
 | 
						b.Unlock()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
 | 
				
			||||||
 | 
						for ID, channel := range channels {
 | 
				
			||||||
 | 
							if !exists[ID] {
 | 
				
			||||||
 | 
								b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
 | 
				
			||||||
 | 
								time.Sleep(time.Duration(b.GetInt("JoinDelay")) * time.Millisecond)
 | 
				
			||||||
 | 
								err := b.JoinChannel(channel)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								exists[ID] = true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) GetConfigKey(key string) string {
 | 
				
			||||||
 | 
						return b.Account + "." + key
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) IsKeySet(key string) bool {
 | 
				
			||||||
 | 
						return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) GetBool(key string) bool {
 | 
				
			||||||
 | 
						val, ok := b.Config.GetBool(b.GetConfigKey(key))
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							val, _ = b.Config.GetBool("general." + key)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return val
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) GetInt(key string) int {
 | 
				
			||||||
 | 
						val, ok := b.Config.GetInt(b.GetConfigKey(key))
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							val, _ = b.Config.GetInt("general." + key)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return val
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) GetString(key string) string {
 | 
				
			||||||
 | 
						val, ok := b.Config.GetString(b.GetConfigKey(key))
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							val, _ = b.Config.GetString("general." + key)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return val
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) GetStringSlice(key string) []string {
 | 
				
			||||||
 | 
						val, ok := b.Config.GetStringSlice(b.GetConfigKey(key))
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							val, _ = b.Config.GetStringSlice("general." + key)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return val
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bridge) GetStringSlice2D(key string) [][]string {
 | 
				
			||||||
 | 
						val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key))
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							val, _ = b.Config.GetStringSlice2D("general." + key)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return val
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										441
									
								
								bridge/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										441
									
								
								bridge/config/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,441 @@
 | 
				
			|||||||
 | 
					package config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/fsnotify/fsnotify"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"github.com/spf13/viper"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						EventJoinLeave         = "join_leave"
 | 
				
			||||||
 | 
						EventTopicChange       = "topic_change"
 | 
				
			||||||
 | 
						EventFailure           = "failure"
 | 
				
			||||||
 | 
						EventFileFailureSize   = "file_failure_size"
 | 
				
			||||||
 | 
						EventAvatarDownload    = "avatar_download"
 | 
				
			||||||
 | 
						EventRejoinChannels    = "rejoin_channels"
 | 
				
			||||||
 | 
						EventUserAction        = "user_action"
 | 
				
			||||||
 | 
						EventMsgDelete         = "msg_delete"
 | 
				
			||||||
 | 
						EventFileDelete        = "file_delete"
 | 
				
			||||||
 | 
						EventAPIConnected      = "api_connected"
 | 
				
			||||||
 | 
						EventUserTyping        = "user_typing"
 | 
				
			||||||
 | 
						EventGetChannelMembers = "get_channel_members"
 | 
				
			||||||
 | 
						EventNoticeIRC         = "notice_irc"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ParentIDNotFound = "msg-parent-not-found"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Message struct {
 | 
				
			||||||
 | 
						Text      string    `json:"text"`
 | 
				
			||||||
 | 
						Channel   string    `json:"channel"`
 | 
				
			||||||
 | 
						Username  string    `json:"username"`
 | 
				
			||||||
 | 
						UserID    string    `json:"userid"` // userid on the bridge
 | 
				
			||||||
 | 
						Avatar    string    `json:"avatar"`
 | 
				
			||||||
 | 
						Account   string    `json:"account"`
 | 
				
			||||||
 | 
						Event     string    `json:"event"`
 | 
				
			||||||
 | 
						Protocol  string    `json:"protocol"`
 | 
				
			||||||
 | 
						Gateway   string    `json:"gateway"`
 | 
				
			||||||
 | 
						ParentID  string    `json:"parent_id"`
 | 
				
			||||||
 | 
						Timestamp time.Time `json:"timestamp"`
 | 
				
			||||||
 | 
						ID        string    `json:"id"`
 | 
				
			||||||
 | 
						Extra     map[string][]interface{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m Message) ParentNotFound() bool {
 | 
				
			||||||
 | 
						return m.ParentID == ParentIDNotFound
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m Message) ParentValid() bool {
 | 
				
			||||||
 | 
						return m.ParentID != "" && !m.ParentNotFound()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FileInfo struct {
 | 
				
			||||||
 | 
						Name     string
 | 
				
			||||||
 | 
						Data     *[]byte
 | 
				
			||||||
 | 
						Comment  string
 | 
				
			||||||
 | 
						URL      string
 | 
				
			||||||
 | 
						Size     int64
 | 
				
			||||||
 | 
						Avatar   bool
 | 
				
			||||||
 | 
						SHA      string
 | 
				
			||||||
 | 
						NativeID string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChannelInfo struct {
 | 
				
			||||||
 | 
						Name        string
 | 
				
			||||||
 | 
						Account     string
 | 
				
			||||||
 | 
						Direction   string
 | 
				
			||||||
 | 
						ID          string
 | 
				
			||||||
 | 
						SameChannel map[string]bool
 | 
				
			||||||
 | 
						Options     ChannelOptions
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChannelMember struct {
 | 
				
			||||||
 | 
						Username    string
 | 
				
			||||||
 | 
						Nick        string
 | 
				
			||||||
 | 
						UserID      string
 | 
				
			||||||
 | 
						ChannelID   string
 | 
				
			||||||
 | 
						ChannelName string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChannelMembers []ChannelMember
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Protocol struct {
 | 
				
			||||||
 | 
						AllowMention           []string // discord
 | 
				
			||||||
 | 
						AuthCode               string   // steam
 | 
				
			||||||
 | 
						BindAddress            string   // mattermost, slack // DEPRECATED
 | 
				
			||||||
 | 
						Buffer                 int      // api
 | 
				
			||||||
 | 
						Charset                string   // irc
 | 
				
			||||||
 | 
						ClientID               string   // msteams
 | 
				
			||||||
 | 
						ColorNicks             bool     // only irc for now
 | 
				
			||||||
 | 
						Debug                  bool     // general
 | 
				
			||||||
 | 
						DebugLevel             int      // only for irc now
 | 
				
			||||||
 | 
						DisableWebPagePreview  bool     // telegram
 | 
				
			||||||
 | 
						EditSuffix             string   // mattermost, slack, discord, telegram, gitter
 | 
				
			||||||
 | 
						EditDisable            bool     // mattermost, slack, discord, telegram, gitter
 | 
				
			||||||
 | 
						HTMLDisable            bool     // matrix
 | 
				
			||||||
 | 
						IconURL                string   // mattermost, slack
 | 
				
			||||||
 | 
						IgnoreFailureOnStart   bool     // general
 | 
				
			||||||
 | 
						IgnoreNicks            string   // all protocols
 | 
				
			||||||
 | 
						IgnoreMessages         string   // all protocols
 | 
				
			||||||
 | 
						Jid                    string   // xmpp
 | 
				
			||||||
 | 
						JoinDelay              string   // all protocols
 | 
				
			||||||
 | 
						Label                  string   // all protocols
 | 
				
			||||||
 | 
						Login                  string   // mattermost, matrix
 | 
				
			||||||
 | 
						LogFile                string   // general
 | 
				
			||||||
 | 
						MediaDownloadBlackList []string
 | 
				
			||||||
 | 
						MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
 | 
				
			||||||
 | 
						MediaDownloadSize      int    // all protocols
 | 
				
			||||||
 | 
						MediaServerDownload    string
 | 
				
			||||||
 | 
						MediaServerUpload      string
 | 
				
			||||||
 | 
						MediaConvertTgs        string     // telegram
 | 
				
			||||||
 | 
						MediaConvertWebPToPNG  bool       // telegram
 | 
				
			||||||
 | 
						MessageDelay           int        // IRC, time in millisecond to wait between messages
 | 
				
			||||||
 | 
						MessageFormat          string     // telegram
 | 
				
			||||||
 | 
						MessageLength          int        // IRC, max length of a message allowed
 | 
				
			||||||
 | 
						MessageQueue           int        // IRC, size of message queue for flood control
 | 
				
			||||||
 | 
						MessageSplit           bool       // IRC, split long messages with newlines on MessageLength instead of clipping
 | 
				
			||||||
 | 
						Muc                    string     // xmpp
 | 
				
			||||||
 | 
						MxID                   string     // matrix
 | 
				
			||||||
 | 
						Name                   string     // all protocols
 | 
				
			||||||
 | 
						Nick                   string     // all protocols
 | 
				
			||||||
 | 
						NickFormatter          string     // mattermost, slack
 | 
				
			||||||
 | 
						NickServNick           string     // IRC
 | 
				
			||||||
 | 
						NickServUsername       string     // IRC
 | 
				
			||||||
 | 
						NickServPassword       string     // IRC
 | 
				
			||||||
 | 
						NicksPerRow            int        // mattermost, slack
 | 
				
			||||||
 | 
						NoHomeServerSuffix     bool       // matrix
 | 
				
			||||||
 | 
						NoSendJoinPart         bool       // all protocols
 | 
				
			||||||
 | 
						NoTLS                  bool       // mattermost, xmpp
 | 
				
			||||||
 | 
						Password               string     // IRC,mattermost,XMPP,matrix
 | 
				
			||||||
 | 
						PrefixMessagesWithNick bool       // mattemost, slack
 | 
				
			||||||
 | 
						PreserveThreading      bool       // slack
 | 
				
			||||||
 | 
						Protocol               string     // all protocols
 | 
				
			||||||
 | 
						QuoteDisable           bool       // telegram
 | 
				
			||||||
 | 
						QuoteFormat            string     // telegram
 | 
				
			||||||
 | 
						QuoteLengthLimit       int        // telegram
 | 
				
			||||||
 | 
						RealName               string     // IRC
 | 
				
			||||||
 | 
						RejoinDelay            int        // IRC
 | 
				
			||||||
 | 
						ReplaceMessages        [][]string // all protocols
 | 
				
			||||||
 | 
						ReplaceNicks           [][]string // all protocols
 | 
				
			||||||
 | 
						RemoteNickFormat       string     // all protocols
 | 
				
			||||||
 | 
						RunCommands            []string   // IRC
 | 
				
			||||||
 | 
						Server                 string     // IRC,mattermost,XMPP,discord,matrix
 | 
				
			||||||
 | 
						SessionFile            string     // msteams,whatsapp
 | 
				
			||||||
 | 
						ShowJoinPart           bool       // all protocols
 | 
				
			||||||
 | 
						ShowTopicChange        bool       // slack
 | 
				
			||||||
 | 
						ShowUserTyping         bool       // slack
 | 
				
			||||||
 | 
						ShowEmbeds             bool       // discord
 | 
				
			||||||
 | 
						SkipTLSVerify          bool       // IRC, mattermost
 | 
				
			||||||
 | 
						SkipVersionCheck       bool       // mattermost
 | 
				
			||||||
 | 
						StripNick              bool       // all protocols
 | 
				
			||||||
 | 
						StripMarkdown          bool       // irc
 | 
				
			||||||
 | 
						SyncTopic              bool       // slack
 | 
				
			||||||
 | 
						TengoModifyMessage     string     // general
 | 
				
			||||||
 | 
						Team                   string     // mattermost, keybase
 | 
				
			||||||
 | 
						TeamID                 string     // msteams
 | 
				
			||||||
 | 
						TenantID               string     // msteams
 | 
				
			||||||
 | 
						Token                  string     // gitter, slack, discord, api, matrix
 | 
				
			||||||
 | 
						Topic                  string     // zulip
 | 
				
			||||||
 | 
						URL                    string     // mattermost, slack // DEPRECATED
 | 
				
			||||||
 | 
						UseAPI                 bool       // mattermost, slack
 | 
				
			||||||
 | 
						UseLocalAvatar         []string   // discord
 | 
				
			||||||
 | 
						UseSASL                bool       // IRC
 | 
				
			||||||
 | 
						UseTLS                 bool       // IRC
 | 
				
			||||||
 | 
						UseDiscriminator       bool       // discord
 | 
				
			||||||
 | 
						UseFirstName           bool       // telegram
 | 
				
			||||||
 | 
						UseUserName            bool       // discord, matrix, mattermost
 | 
				
			||||||
 | 
						UseInsecureURL         bool       // telegram
 | 
				
			||||||
 | 
						UserName               string     // IRC
 | 
				
			||||||
 | 
						VerboseJoinPart        bool       // IRC
 | 
				
			||||||
 | 
						WebhookBindAddress     string     // mattermost, slack
 | 
				
			||||||
 | 
						WebhookURL             string     // mattermost, slack
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChannelOptions struct {
 | 
				
			||||||
 | 
						Key        string // irc, xmpp
 | 
				
			||||||
 | 
						WebhookURL string // discord
 | 
				
			||||||
 | 
						Topic      string // zulip
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bridge struct {
 | 
				
			||||||
 | 
						Account     string
 | 
				
			||||||
 | 
						Channel     string
 | 
				
			||||||
 | 
						Options     ChannelOptions
 | 
				
			||||||
 | 
						SameChannel bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Gateway struct {
 | 
				
			||||||
 | 
						Name   string
 | 
				
			||||||
 | 
						Enable bool
 | 
				
			||||||
 | 
						In     []Bridge
 | 
				
			||||||
 | 
						Out    []Bridge
 | 
				
			||||||
 | 
						InOut  []Bridge
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Tengo struct {
 | 
				
			||||||
 | 
						InMessage        string
 | 
				
			||||||
 | 
						Message          string
 | 
				
			||||||
 | 
						RemoteNickFormat string
 | 
				
			||||||
 | 
						OutMessage       string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SameChannelGateway struct {
 | 
				
			||||||
 | 
						Name     string
 | 
				
			||||||
 | 
						Enable   bool
 | 
				
			||||||
 | 
						Channels []string
 | 
				
			||||||
 | 
						Accounts []string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BridgeValues struct {
 | 
				
			||||||
 | 
						API                map[string]Protocol
 | 
				
			||||||
 | 
						IRC                map[string]Protocol
 | 
				
			||||||
 | 
						Mattermost         map[string]Protocol
 | 
				
			||||||
 | 
						Matrix             map[string]Protocol
 | 
				
			||||||
 | 
						Slack              map[string]Protocol
 | 
				
			||||||
 | 
						SlackLegacy        map[string]Protocol
 | 
				
			||||||
 | 
						Steam              map[string]Protocol
 | 
				
			||||||
 | 
						Gitter             map[string]Protocol
 | 
				
			||||||
 | 
						XMPP               map[string]Protocol
 | 
				
			||||||
 | 
						Discord            map[string]Protocol
 | 
				
			||||||
 | 
						Telegram           map[string]Protocol
 | 
				
			||||||
 | 
						Rocketchat         map[string]Protocol
 | 
				
			||||||
 | 
						SSHChat            map[string]Protocol
 | 
				
			||||||
 | 
						WhatsApp           map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
 | 
				
			||||||
 | 
						Zulip              map[string]Protocol
 | 
				
			||||||
 | 
						Keybase            map[string]Protocol
 | 
				
			||||||
 | 
						Mumble             map[string]Protocol
 | 
				
			||||||
 | 
						General            Protocol
 | 
				
			||||||
 | 
						Tengo              Tengo
 | 
				
			||||||
 | 
						Gateway            []Gateway
 | 
				
			||||||
 | 
						SameChannelGateway []SameChannelGateway
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Config interface {
 | 
				
			||||||
 | 
						Viper() *viper.Viper
 | 
				
			||||||
 | 
						BridgeValues() *BridgeValues
 | 
				
			||||||
 | 
						IsKeySet(key string) bool
 | 
				
			||||||
 | 
						GetBool(key string) (bool, bool)
 | 
				
			||||||
 | 
						GetInt(key string) (int, bool)
 | 
				
			||||||
 | 
						GetString(key string) (string, bool)
 | 
				
			||||||
 | 
						GetStringSlice(key string) ([]string, bool)
 | 
				
			||||||
 | 
						GetStringSlice2D(key string) ([][]string, bool)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type config struct {
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger *logrus.Entry
 | 
				
			||||||
 | 
						v      *viper.Viper
 | 
				
			||||||
 | 
						cv     *BridgeValues
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewConfig instantiates a new configuration based on the specified configuration file path.
 | 
				
			||||||
 | 
					func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
 | 
				
			||||||
 | 
						logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						viper.SetConfigFile(cfgfile)
 | 
				
			||||||
 | 
						input, err := ioutil.ReadFile(cfgfile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Fatalf("Failed to read configuration file: %#v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cfgtype := detectConfigType(cfgfile)
 | 
				
			||||||
 | 
						mycfg := newConfigFromString(logger, input, cfgtype)
 | 
				
			||||||
 | 
						if mycfg.cv.General.LogFile != "" {
 | 
				
			||||||
 | 
							logfile, err := os.OpenFile(mycfg.cv.General.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								logger.Info("Opening log file ", mycfg.cv.General.LogFile)
 | 
				
			||||||
 | 
								rootLogger.Out = logfile
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								logger.Warn("Failed to open ", mycfg.cv.General.LogFile)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if mycfg.cv.General.MediaDownloadSize == 0 {
 | 
				
			||||||
 | 
							mycfg.cv.General.MediaDownloadSize = 1000000
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						viper.WatchConfig()
 | 
				
			||||||
 | 
						viper.OnConfigChange(func(e fsnotify.Event) {
 | 
				
			||||||
 | 
							logger.Println("Config file changed:", e.Name)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return mycfg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// detectConfigType detects JSON and YAML formats, defaults to TOML.
 | 
				
			||||||
 | 
					func detectConfigType(cfgfile string) string {
 | 
				
			||||||
 | 
						fileExt := filepath.Ext(cfgfile)
 | 
				
			||||||
 | 
						switch fileExt {
 | 
				
			||||||
 | 
						case ".json":
 | 
				
			||||||
 | 
							return "json"
 | 
				
			||||||
 | 
						case ".yaml", ".yml":
 | 
				
			||||||
 | 
							return "yaml"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "toml"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewConfigFromString instantiates a new configuration based on the specified string.
 | 
				
			||||||
 | 
					func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
 | 
				
			||||||
 | 
						logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
 | 
				
			||||||
 | 
						return newConfigFromString(logger, input, "toml")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config {
 | 
				
			||||||
 | 
						viper.SetConfigType(cfgtype)
 | 
				
			||||||
 | 
						viper.SetEnvPrefix("matterbridge")
 | 
				
			||||||
 | 
						viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
 | 
				
			||||||
 | 
						viper.AutomaticEnv()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
 | 
				
			||||||
 | 
							logger.Fatalf("Failed to parse the configuration: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cfg := &BridgeValues{}
 | 
				
			||||||
 | 
						if err := viper.Unmarshal(cfg); err != nil {
 | 
				
			||||||
 | 
							logger.Fatalf("Failed to load the configuration: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &config{
 | 
				
			||||||
 | 
							logger: logger,
 | 
				
			||||||
 | 
							v:      viper.GetViper(),
 | 
				
			||||||
 | 
							cv:     cfg,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) BridgeValues() *BridgeValues {
 | 
				
			||||||
 | 
						return c.cv
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) Viper() *viper.Viper {
 | 
				
			||||||
 | 
						return c.v
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) IsKeySet(key string) bool {
 | 
				
			||||||
 | 
						c.RLock()
 | 
				
			||||||
 | 
						defer c.RUnlock()
 | 
				
			||||||
 | 
						return c.v.IsSet(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) GetBool(key string) (bool, bool) {
 | 
				
			||||||
 | 
						c.RLock()
 | 
				
			||||||
 | 
						defer c.RUnlock()
 | 
				
			||||||
 | 
						return c.v.GetBool(key), c.v.IsSet(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) GetInt(key string) (int, bool) {
 | 
				
			||||||
 | 
						c.RLock()
 | 
				
			||||||
 | 
						defer c.RUnlock()
 | 
				
			||||||
 | 
						return c.v.GetInt(key), c.v.IsSet(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) GetString(key string) (string, bool) {
 | 
				
			||||||
 | 
						c.RLock()
 | 
				
			||||||
 | 
						defer c.RUnlock()
 | 
				
			||||||
 | 
						return c.v.GetString(key), c.v.IsSet(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) GetStringSlice(key string) ([]string, bool) {
 | 
				
			||||||
 | 
						c.RLock()
 | 
				
			||||||
 | 
						defer c.RUnlock()
 | 
				
			||||||
 | 
						return c.v.GetStringSlice(key), c.v.IsSet(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *config) GetStringSlice2D(key string) ([][]string, bool) {
 | 
				
			||||||
 | 
						c.RLock()
 | 
				
			||||||
 | 
						defer c.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res, ok := c.v.Get(key).([]interface{})
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var result [][]string
 | 
				
			||||||
 | 
						for _, entry := range res {
 | 
				
			||||||
 | 
							result2 := []string{}
 | 
				
			||||||
 | 
							for _, entry2 := range entry.([]interface{}) {
 | 
				
			||||||
 | 
								result2 = append(result2, entry2.(string))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							result = append(result, result2)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetIconURL(msg *Message, iconURL string) string {
 | 
				
			||||||
 | 
						info := strings.Split(msg.Account, ".")
 | 
				
			||||||
 | 
						protocol := info[0]
 | 
				
			||||||
 | 
						name := info[1]
 | 
				
			||||||
 | 
						iconURL = strings.Replace(iconURL, "{NICK}", msg.Username, -1)
 | 
				
			||||||
 | 
						iconURL = strings.Replace(iconURL, "{BRIDGE}", name, -1)
 | 
				
			||||||
 | 
						iconURL = strings.Replace(iconURL, "{PROTOCOL}", protocol, -1)
 | 
				
			||||||
 | 
						return iconURL
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TestConfig struct {
 | 
				
			||||||
 | 
						Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Overrides map[string]interface{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *TestConfig) IsKeySet(key string) bool {
 | 
				
			||||||
 | 
						_, ok := c.Overrides[key]
 | 
				
			||||||
 | 
						return ok || c.Config.IsKeySet(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *TestConfig) GetBool(key string) (bool, bool) {
 | 
				
			||||||
 | 
						val, ok := c.Overrides[key]
 | 
				
			||||||
 | 
						if ok {
 | 
				
			||||||
 | 
							return val.(bool), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c.Config.GetBool(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *TestConfig) GetInt(key string) (int, bool) {
 | 
				
			||||||
 | 
						if val, ok := c.Overrides[key]; ok {
 | 
				
			||||||
 | 
							return val.(int), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c.Config.GetInt(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *TestConfig) GetString(key string) (string, bool) {
 | 
				
			||||||
 | 
						if val, ok := c.Overrides[key]; ok {
 | 
				
			||||||
 | 
							return val.(string), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c.Config.GetString(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *TestConfig) GetStringSlice(key string) ([]string, bool) {
 | 
				
			||||||
 | 
						if val, ok := c.Overrides[key]; ok {
 | 
				
			||||||
 | 
							return val.([]string), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c.Config.GetStringSlice(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *TestConfig) GetStringSlice2D(key string) ([][]string, bool) {
 | 
				
			||||||
 | 
						if val, ok := c.Overrides[key]; ok {
 | 
				
			||||||
 | 
							return val.([][]string), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c.Config.GetStringSlice2D(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										384
									
								
								bridge/discord/discord.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								bridge/discord/discord.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,384 @@
 | 
				
			|||||||
 | 
					package bdiscord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/discord/transmitter"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
						lru "github.com/hashicorp/golang-lru"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						MessageLength = 1950
 | 
				
			||||||
 | 
						cFileUpload   = "file_upload"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bdiscord struct {
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c *discordgo.Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						nick    string
 | 
				
			||||||
 | 
						userID  string
 | 
				
			||||||
 | 
						guildID string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channelsMutex  sync.RWMutex
 | 
				
			||||||
 | 
						channels       []*discordgo.Channel
 | 
				
			||||||
 | 
						channelInfoMap map[string]*config.ChannelInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						membersMutex  sync.RWMutex
 | 
				
			||||||
 | 
						userMemberMap map[string]*discordgo.Member
 | 
				
			||||||
 | 
						nickMemberMap map[string]*discordgo.Member
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Webhook specific logic
 | 
				
			||||||
 | 
						useAutoWebhooks bool
 | 
				
			||||||
 | 
						transmitter     *transmitter.Transmitter
 | 
				
			||||||
 | 
						cache           *lru.Cache
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						newCache, err := lru.New(5000)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							cfg.Log.Fatalf("Could not create LRU cache: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b := &Bdiscord{
 | 
				
			||||||
 | 
							Config: cfg,
 | 
				
			||||||
 | 
							cache:  newCache,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.userMemberMap = make(map[string]*discordgo.Member)
 | 
				
			||||||
 | 
						b.nickMemberMap = make(map[string]*discordgo.Member)
 | 
				
			||||||
 | 
						b.channelInfoMap = make(map[string]*config.ChannelInfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.useAutoWebhooks = b.GetBool("AutoWebhooks")
 | 
				
			||||||
 | 
						if b.useAutoWebhooks {
 | 
				
			||||||
 | 
							b.Log.Debug("Using automatic webhooks")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) Connect() error {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						token := b.GetString("Token")
 | 
				
			||||||
 | 
						b.Log.Info("Connecting")
 | 
				
			||||||
 | 
						if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
 | 
				
			||||||
 | 
							token = "Bot " + b.GetString("Token")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// if we have a User token, remove the `Bot` prefix
 | 
				
			||||||
 | 
						if strings.HasPrefix(b.GetString("Token"), "User ") {
 | 
				
			||||||
 | 
							token = strings.Replace(b.GetString("Token"), "User ", "", -1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.c, err = discordgo.New(token)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						b.c.AddHandler(b.messageCreate)
 | 
				
			||||||
 | 
						b.c.AddHandler(b.messageTyping)
 | 
				
			||||||
 | 
						b.c.AddHandler(b.messageUpdate)
 | 
				
			||||||
 | 
						b.c.AddHandler(b.messageDelete)
 | 
				
			||||||
 | 
						b.c.AddHandler(b.messageDeleteBulk)
 | 
				
			||||||
 | 
						b.c.AddHandler(b.memberAdd)
 | 
				
			||||||
 | 
						b.c.AddHandler(b.memberRemove)
 | 
				
			||||||
 | 
						b.c.AddHandler(b.memberUpdate)
 | 
				
			||||||
 | 
						if b.GetInt("debuglevel") == 1 {
 | 
				
			||||||
 | 
							b.c.AddHandler(b.messageEvent)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Add privileged intent for guild member tracking. This is needed to track nicks
 | 
				
			||||||
 | 
						// for display names and @mention translation
 | 
				
			||||||
 | 
						b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
 | 
				
			||||||
 | 
							discordgo.IntentsGuildMembers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = b.c.Open()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						guilds, err := b.c.UserGuilds(100, "", "")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						userinfo, err := b.c.User("@me")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
 | 
				
			||||||
 | 
						b.nick = userinfo.Username
 | 
				
			||||||
 | 
						b.userID = userinfo.ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try and find this account's guild, and populate channels
 | 
				
			||||||
 | 
						b.channelsMutex.Lock()
 | 
				
			||||||
 | 
						for _, guild := range guilds {
 | 
				
			||||||
 | 
							// Skip, if the server name does not match the visible name or the ID
 | 
				
			||||||
 | 
							if guild.Name != serverName && guild.ID != serverName {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Complain about an ambiguous Server setting. Two Discord servers could have the same title!
 | 
				
			||||||
 | 
							// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID.
 | 
				
			||||||
 | 
							if b.guildID != "" {
 | 
				
			||||||
 | 
								return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Getting this guild's channel could result in a permission error
 | 
				
			||||||
 | 
							b.channels, err = b.c.GuildChannels(guild.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.guildID = guild.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.channelsMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we couldn't find a guild, we print extra debug information and return a nice error
 | 
				
			||||||
 | 
						if b.guildID == "" {
 | 
				
			||||||
 | 
							err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server"))
 | 
				
			||||||
 | 
							b.Log.Error(err.Error())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Print all of the possible server values
 | 
				
			||||||
 | 
							b.Log.Info("Possible server values:")
 | 
				
			||||||
 | 
							for _, guild := range guilds {
 | 
				
			||||||
 | 
								b.Log.Infof("\t- Server=%#v # by name", guild.Name)
 | 
				
			||||||
 | 
								b.Log.Infof("\t- Server=%#v # by ID", guild.ID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If there are no results, we should say that
 | 
				
			||||||
 | 
							if len(guilds) == 0 {
 | 
				
			||||||
 | 
								b.Log.Info("\t- (none found)")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
 | 
				
			||||||
 | 
						// but we stopped doing that due to Discord making rate limits more aggressive.
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
 | 
				
			||||||
 | 
						// This is no longer possible.
 | 
				
			||||||
 | 
						if b.GetString("WebhookURL") != "" {
 | 
				
			||||||
 | 
							message := "The global WebhookURL setting has been removed. "
 | 
				
			||||||
 | 
							message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
 | 
				
			||||||
 | 
							message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
 | 
				
			||||||
 | 
							b.Log.Errorln(message)
 | 
				
			||||||
 | 
							return fmt.Errorf("use of removed WebhookURL setting")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetInt("debuglevel") == 2 {
 | 
				
			||||||
 | 
							b.Log.Debug("enabling even more discord debug")
 | 
				
			||||||
 | 
							b.c.Debug = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialise webhook management
 | 
				
			||||||
 | 
						b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
 | 
				
			||||||
 | 
						b.transmitter.Log = b.Log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var webhookChannelIDs []string
 | 
				
			||||||
 | 
						for _, channel := range b.Channels {
 | 
				
			||||||
 | 
							channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If a WebhookURL was not explicitly provided for this channel,
 | 
				
			||||||
 | 
							// there are two options: just a regular bot message (ugly) or this is should be webhook sent
 | 
				
			||||||
 | 
							if channel.Options.WebhookURL == "" {
 | 
				
			||||||
 | 
								// If it should be webhook sent, we should enforce this via the transmitter
 | 
				
			||||||
 | 
								if b.useAutoWebhooks {
 | 
				
			||||||
 | 
									webhookChannelIDs = append(webhookChannelIDs, channelID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							whID, whToken, ok := b.splitURL(channel.Options.WebhookURL)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.transmitter.AddWebhook(channelID, &discordgo.Webhook{
 | 
				
			||||||
 | 
								ID:        whID,
 | 
				
			||||||
 | 
								Token:     whToken,
 | 
				
			||||||
 | 
								GuildID:   b.guildID,
 | 
				
			||||||
 | 
								ChannelID: channelID,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.useAutoWebhooks {
 | 
				
			||||||
 | 
							err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.WithError(err).Println("transmitter could not refresh guild webhooks")
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Obtaining guild members and initializing nickname mapping.
 | 
				
			||||||
 | 
						b.membersMutex.Lock()
 | 
				
			||||||
 | 
						defer b.membersMutex.Unlock()
 | 
				
			||||||
 | 
						members, err := b.c.GuildMembers(b.guildID, "", 1000)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Error("Error obtaining server members: ", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, member := range members {
 | 
				
			||||||
 | 
							if member == nil {
 | 
				
			||||||
 | 
								b.Log.Warnf("Skipping missing information for a user.")
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.userMemberMap[member.User.ID] = member
 | 
				
			||||||
 | 
							b.nickMemberMap[member.User.Username] = member
 | 
				
			||||||
 | 
							if member.Nick != "" {
 | 
				
			||||||
 | 
								b.nickMemberMap[member.Nick] = member
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) Disconnect() error {
 | 
				
			||||||
 | 
						return b.c.Close()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						b.channelsMutex.Lock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.channelInfoMap[channel.ID] = &channel
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channelID := b.getChannelID(msg.Channel)
 | 
				
			||||||
 | 
						if channelID == "" {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("Could not find channelID for %v", msg.Channel)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserTyping {
 | 
				
			||||||
 | 
							if b.GetBool("ShowUserTyping") {
 | 
				
			||||||
 | 
								err := b.c.ChannelTyping(channelID)
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make a action /me of the message
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
							msg.Text = "_" + msg.Text + "_"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle prefix hint for unthreaded messages.
 | 
				
			||||||
 | 
						if msg.ParentNotFound() {
 | 
				
			||||||
 | 
							msg.ParentID = ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use webhook to send the message
 | 
				
			||||||
 | 
						useWebhooks := b.shouldMessageUseWebhooks(&msg)
 | 
				
			||||||
 | 
						if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" {
 | 
				
			||||||
 | 
							return b.handleEventWebhook(&msg, channelID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.handleEventBotUser(&msg, channelID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleEventDirect handles events via the bot user
 | 
				
			||||||
 | 
					func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("Broadcasting using token (API)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err := b.c.ChannelMessageDelete(channelID, msg.ID)
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete a file
 | 
				
			||||||
 | 
						if msg.Event == config.EventFileDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok {
 | 
				
			||||||
 | 
								err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert
 | 
				
			||||||
 | 
								b.cache.Remove(cFileUpload + msg.ID)
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return "", fmt.Errorf("file %s not found", msg.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file if it exists
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(msg, b.General) {
 | 
				
			||||||
 | 
								rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
								if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// check if we have files to upload (from slack, telegram or mattermost)
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return b.handleUploadFile(msg, channelID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
						msg.Text = b.replaceUserMentions(msg.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
 | 
				
			||||||
 | 
							return msg.ID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						m := discordgo.MessageSend{
 | 
				
			||||||
 | 
							Content:         msg.Username + msg.Text,
 | 
				
			||||||
 | 
							AllowedMentions: b.getAllowedMentions(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.ParentValid() {
 | 
				
			||||||
 | 
							m.Reference = &discordgo.MessageReference{
 | 
				
			||||||
 | 
								MessageID: msg.ParentID,
 | 
				
			||||||
 | 
								ChannelID: channelID,
 | 
				
			||||||
 | 
								GuildID:   b.guildID,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post normal message
 | 
				
			||||||
 | 
						res, err := b.c.ChannelMessageSendComplex(channelID, &m)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return res.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleUploadFile handles native upload of files
 | 
				
			||||||
 | 
					func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							file := discordgo.File{
 | 
				
			||||||
 | 
								Name:        fi.Name,
 | 
				
			||||||
 | 
								ContentType: "",
 | 
				
			||||||
 | 
								Reader:      bytes.NewReader(*fi.Data),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							m := discordgo.MessageSend{
 | 
				
			||||||
 | 
								Content:         msg.Username + fi.Comment,
 | 
				
			||||||
 | 
								Files:           []*discordgo.File{&file},
 | 
				
			||||||
 | 
								AllowedMentions: b.getAllowedMentions(),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							res, err := b.c.ChannelMessageSendComplex(channelID, &m)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", fmt.Errorf("file upload failed: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// link file_upload_nativeID (file ID from the original bridge) to our upload id
 | 
				
			||||||
 | 
							// so that we can remove this later when it eg needs to be deleted
 | 
				
			||||||
 | 
							b.cache.Add(cFileUpload+fi.NativeID, res.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										281
									
								
								bridge/discord/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								bridge/discord/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,281 @@
 | 
				
			|||||||
 | 
					package bdiscord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
						"github.com/davecgh/go-spew/spew"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring messageDelete because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
 | 
				
			||||||
 | 
						rmsg.Channel = b.getChannelName(m.ChannelID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
 | 
				
			||||||
 | 
					func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring messageDeleteBulk because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, msgID := range m.Messages {
 | 
				
			||||||
 | 
							rmsg := config.Message{
 | 
				
			||||||
 | 
								Account: b.Account,
 | 
				
			||||||
 | 
								ID:      msgID,
 | 
				
			||||||
 | 
								Event:   config.EventMsgDelete,
 | 
				
			||||||
 | 
								Text:    config.EventMsgDelete,
 | 
				
			||||||
 | 
								Channel: b.getChannelName(m.ChannelID),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
							b.Remote <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) {
 | 
				
			||||||
 | 
						b.Log.Debug(spew.Sdump(m.Struct))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring messageTyping because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !b.GetBool("ShowUserTyping") {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore our own typing messages
 | 
				
			||||||
 | 
						if m.UserID == b.userID {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping}
 | 
				
			||||||
 | 
						rmsg.Channel = b.getChannelName(m.ChannelID)
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring messageUpdate because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetBool("EditDisable") {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// only when message is actually edited
 | 
				
			||||||
 | 
						if m.Message.EditedTimestamp != nil {
 | 
				
			||||||
 | 
							b.Log.Debugf("Sending edit message")
 | 
				
			||||||
 | 
							m.Content += b.GetString("EditSuffix")
 | 
				
			||||||
 | 
							msg := &discordgo.MessageCreate{
 | 
				
			||||||
 | 
								Message: m.Message,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.messageCreate(s, msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring messageCreate because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// not relay our own messages
 | 
				
			||||||
 | 
						if m.Author.Username == b.nick {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// if using webhooks, do not relay if it's ours
 | 
				
			||||||
 | 
						if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// add the url of the attachments to content
 | 
				
			||||||
 | 
						if len(m.Attachments) > 0 {
 | 
				
			||||||
 | 
							for _, attach := range m.Attachments {
 | 
				
			||||||
 | 
								m.Content = m.Content + "\n" + attach.URL
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("== Receiving event %#v", m.Message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if m.Content != "" {
 | 
				
			||||||
 | 
							m.Message.Content = b.replaceChannelMentions(m.Message.Content)
 | 
				
			||||||
 | 
							rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
 | 
				
			||||||
 | 
								rmsg.Text = m.ContentWithMentionsReplaced()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// set channel name
 | 
				
			||||||
 | 
						rmsg.Channel = b.getChannelName(m.ChannelID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fromWebhook := m.WebhookID != ""
 | 
				
			||||||
 | 
						if !fromWebhook && !b.GetBool("UseUserName") {
 | 
				
			||||||
 | 
							rmsg.Username = b.getNick(m.Author, m.GuildID)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							rmsg.Username = m.Author.Username
 | 
				
			||||||
 | 
							if !fromWebhook && b.GetBool("UseDiscriminator") {
 | 
				
			||||||
 | 
								rmsg.Username += "#" + m.Author.Discriminator
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if we have embedded content add it to text
 | 
				
			||||||
 | 
						if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
 | 
				
			||||||
 | 
							for _, embed := range m.Message.Embeds {
 | 
				
			||||||
 | 
								rmsg.Text += handleEmbed(embed)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// no empty messages
 | 
				
			||||||
 | 
						if rmsg.Text == "" {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// do we have a /me action
 | 
				
			||||||
 | 
						var ok bool
 | 
				
			||||||
 | 
						rmsg.Text, ok = b.replaceAction(rmsg.Text)
 | 
				
			||||||
 | 
						if ok {
 | 
				
			||||||
 | 
							rmsg.Event = config.EventUserAction
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Replace emotes
 | 
				
			||||||
 | 
						rmsg.Text = replaceEmotes(rmsg.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add our parent id if it exists, and if it's not referring to a message in another channel
 | 
				
			||||||
 | 
						if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID {
 | 
				
			||||||
 | 
							rmsg.ParentID = ref.MessageID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if m.Member == nil {
 | 
				
			||||||
 | 
							b.Log.Warnf("Received member update with no member information: %#v", m)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.membersMutex.Lock()
 | 
				
			||||||
 | 
						defer b.membersMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if currMember, ok := b.userMemberMap[m.Member.User.ID]; ok {
 | 
				
			||||||
 | 
							b.Log.Debugf(
 | 
				
			||||||
 | 
								"%s: memberupdate: user %s (nick %s) changes nick to %s",
 | 
				
			||||||
 | 
								b.Account,
 | 
				
			||||||
 | 
								m.Member.User.Username,
 | 
				
			||||||
 | 
								b.userMemberMap[m.Member.User.ID].Nick,
 | 
				
			||||||
 | 
								m.Member.Nick,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							delete(b.nickMemberMap, currMember.User.Username)
 | 
				
			||||||
 | 
							delete(b.nickMemberMap, currMember.Nick)
 | 
				
			||||||
 | 
							delete(b.userMemberMap, m.Member.User.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.userMemberMap[m.Member.User.ID] = m.Member
 | 
				
			||||||
 | 
						b.nickMemberMap[m.Member.User.Username] = m.Member
 | 
				
			||||||
 | 
						if m.Member.Nick != "" {
 | 
				
			||||||
 | 
							b.nickMemberMap[m.Member.Nick] = m.Member
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring memberAdd because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetBool("nosendjoinpart") {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if m.Member == nil {
 | 
				
			||||||
 | 
							b.Log.Warnf("Received member update with no member information: %#v", m)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						username := m.Member.User.Username
 | 
				
			||||||
 | 
						if m.Member.Nick != "" {
 | 
				
			||||||
 | 
							username = m.Member.Nick
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Event:    config.EventJoinLeave,
 | 
				
			||||||
 | 
							Username: "system",
 | 
				
			||||||
 | 
							Text:     username + " joins",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
 | 
				
			||||||
 | 
						if m.GuildID != b.guildID {
 | 
				
			||||||
 | 
							b.Log.Debugf("Ignoring memberRemove because it originates from a different guild")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetBool("nosendjoinpart") {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if m.Member == nil {
 | 
				
			||||||
 | 
							b.Log.Warnf("Received member update with no member information: %#v", m)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						username := m.Member.User.Username
 | 
				
			||||||
 | 
						if m.Member.Nick != "" {
 | 
				
			||||||
 | 
							username = m.Member.Nick
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Event:    config.EventJoinLeave,
 | 
				
			||||||
 | 
							Username: "system",
 | 
				
			||||||
 | 
							Text:     username + " leaves",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handleEmbed(embed *discordgo.MessageEmbed) string {
 | 
				
			||||||
 | 
						var t []string
 | 
				
			||||||
 | 
						var result string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t = append(t, embed.Title)
 | 
				
			||||||
 | 
						t = append(t, embed.Description)
 | 
				
			||||||
 | 
						t = append(t, embed.URL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						i := 0
 | 
				
			||||||
 | 
						for _, e := range t {
 | 
				
			||||||
 | 
							if e == "" {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							i++
 | 
				
			||||||
 | 
							if i == 1 {
 | 
				
			||||||
 | 
								result += " embed: " + e
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							result += " - " + e
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if result != "" {
 | 
				
			||||||
 | 
							result += "\n"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return result
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										58
									
								
								bridge/discord/handlers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								bridge/discord/handlers_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					package bdiscord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHandleEmbed(t *testing.T) {
 | 
				
			||||||
 | 
						testcases := map[string]struct {
 | 
				
			||||||
 | 
							embed  *discordgo.MessageEmbed
 | 
				
			||||||
 | 
							result string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							"allempty": {
 | 
				
			||||||
 | 
								embed:  &discordgo.MessageEmbed{},
 | 
				
			||||||
 | 
								result: "",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"one": {
 | 
				
			||||||
 | 
								embed: &discordgo.MessageEmbed{
 | 
				
			||||||
 | 
									Title: "blah",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								result: " embed: blah\n",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"two": {
 | 
				
			||||||
 | 
								embed: &discordgo.MessageEmbed{
 | 
				
			||||||
 | 
									Title:       "blah",
 | 
				
			||||||
 | 
									Description: "blah2",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								result: " embed: blah - blah2\n",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"three": {
 | 
				
			||||||
 | 
								embed: &discordgo.MessageEmbed{
 | 
				
			||||||
 | 
									Title:       "blah",
 | 
				
			||||||
 | 
									Description: "blah2",
 | 
				
			||||||
 | 
									URL:         "blah3",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								result: " embed: blah - blah2 - blah3\n",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"twob": {
 | 
				
			||||||
 | 
								embed: &discordgo.MessageEmbed{
 | 
				
			||||||
 | 
									Description: "blah2",
 | 
				
			||||||
 | 
									URL:         "blah3",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								result: " embed: blah2 - blah3\n",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"oneb": {
 | 
				
			||||||
 | 
								embed: &discordgo.MessageEmbed{
 | 
				
			||||||
 | 
									URL: "blah3",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								result: " embed: blah3\n",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for name, tc := range testcases {
 | 
				
			||||||
 | 
							assert.Equalf(t, tc.result, handleEmbed(tc.embed), "Testcases %s", name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										269
									
								
								bridge/discord/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								bridge/discord/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,269 @@
 | 
				
			|||||||
 | 
					package bdiscord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"unicode"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
 | 
				
			||||||
 | 
						// If AllowMention is not specified, then allow all mentions (default Discord behavior)
 | 
				
			||||||
 | 
						if !b.IsKeySet("AllowMention") {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Otherwise, allow only the mentions that are specified
 | 
				
			||||||
 | 
						allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3)
 | 
				
			||||||
 | 
						for _, m := range b.GetStringSlice("AllowMention") {
 | 
				
			||||||
 | 
							switch m {
 | 
				
			||||||
 | 
							case "everyone":
 | 
				
			||||||
 | 
								allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone)
 | 
				
			||||||
 | 
							case "roles":
 | 
				
			||||||
 | 
								allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles)
 | 
				
			||||||
 | 
							case "users":
 | 
				
			||||||
 | 
								allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &discordgo.MessageAllowedMentions{
 | 
				
			||||||
 | 
							Parse: allowedMentionTypes,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
 | 
				
			||||||
 | 
						b.membersMutex.RLock()
 | 
				
			||||||
 | 
						defer b.membersMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if member, ok := b.userMemberMap[user.ID]; ok {
 | 
				
			||||||
 | 
							if member.Nick != "" {
 | 
				
			||||||
 | 
								// Only return if nick is set.
 | 
				
			||||||
 | 
								return member.Nick
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Otherwise return username.
 | 
				
			||||||
 | 
							return user.Username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we didn't find nick, search for it.
 | 
				
			||||||
 | 
						member, err := b.c.GuildMember(guildID, user.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Warnf("Failed to fetch information for member %#v on guild %#v: %s", user, guildID, err)
 | 
				
			||||||
 | 
							return user.Username
 | 
				
			||||||
 | 
						} else if member == nil {
 | 
				
			||||||
 | 
							b.Log.Warnf("Got no information for member %#v", user)
 | 
				
			||||||
 | 
							return user.Username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.userMemberMap[user.ID] = member
 | 
				
			||||||
 | 
						b.nickMemberMap[member.User.Username] = member
 | 
				
			||||||
 | 
						if member.Nick != "" {
 | 
				
			||||||
 | 
							b.nickMemberMap[member.Nick] = member
 | 
				
			||||||
 | 
							return member.Nick
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return user.Username
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
 | 
				
			||||||
 | 
						b.membersMutex.RLock()
 | 
				
			||||||
 | 
						defer b.membersMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if member, ok := b.nickMemberMap[nick]; ok {
 | 
				
			||||||
 | 
							return member, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) getChannelID(name string) string {
 | 
				
			||||||
 | 
						if strings.Contains(name, "/") {
 | 
				
			||||||
 | 
							return b.getCategoryChannelID(name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.channelsMutex.RLock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						idcheck := strings.Split(name, "ID:")
 | 
				
			||||||
 | 
						if len(idcheck) > 1 {
 | 
				
			||||||
 | 
							return idcheck[1]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, channel := range b.channels {
 | 
				
			||||||
 | 
							if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText {
 | 
				
			||||||
 | 
								return channel.ID
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) getCategoryChannelID(name string) string {
 | 
				
			||||||
 | 
						b.channelsMutex.RLock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.RUnlock()
 | 
				
			||||||
 | 
						res := strings.Split(name, "/")
 | 
				
			||||||
 | 
						// shouldn't happen because function should be only called from getChannelID
 | 
				
			||||||
 | 
						if len(res) != 2 {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						catName, chanName := res[0], res[1]
 | 
				
			||||||
 | 
						for _, channel := range b.channels {
 | 
				
			||||||
 | 
							// if we have a parentID, lookup the name of that parent (category)
 | 
				
			||||||
 | 
							// and if it matches return it
 | 
				
			||||||
 | 
							if channel.Name == chanName && channel.ParentID != "" {
 | 
				
			||||||
 | 
								for _, cat := range b.channels {
 | 
				
			||||||
 | 
									if cat.ID == channel.ParentID && cat.Name == catName {
 | 
				
			||||||
 | 
										return channel.ID
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) getChannelName(id string) string {
 | 
				
			||||||
 | 
						b.channelsMutex.RLock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, c := range b.channelInfoMap {
 | 
				
			||||||
 | 
							if c.Name == "ID:"+id {
 | 
				
			||||||
 | 
								// if we have ID: specified in our gateway configuration return this
 | 
				
			||||||
 | 
								return c.Name
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, channel := range b.channels {
 | 
				
			||||||
 | 
							if channel.ID == id {
 | 
				
			||||||
 | 
								return b.getCategoryChannelName(channel.Name, channel.ParentID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
 | 
				
			||||||
 | 
						var usesCat bool
 | 
				
			||||||
 | 
						// do we have a category configuration in the channel config
 | 
				
			||||||
 | 
						for _, c := range b.channelInfoMap {
 | 
				
			||||||
 | 
							if strings.Contains(c.Name, "/") {
 | 
				
			||||||
 | 
								usesCat = true
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// configuration without category, return the normal channel name
 | 
				
			||||||
 | 
						if !usesCat {
 | 
				
			||||||
 | 
							return name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// create a category/channel response
 | 
				
			||||||
 | 
						for _, c := range b.channels {
 | 
				
			||||||
 | 
							if c.ID == parentID {
 | 
				
			||||||
 | 
								name = c.Name + "/" + name
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						// See https://discordapp.com/developers/docs/reference#message-formatting.
 | 
				
			||||||
 | 
						channelMentionRE = regexp.MustCompile("<#[0-9]+>")
 | 
				
			||||||
 | 
						userMentionRE    = regexp.MustCompile("@[^@\n]{1,32}")
 | 
				
			||||||
 | 
						emoteRE          = regexp.MustCompile(`<a?(:\w+:)\d+>`)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) replaceChannelMentions(text string) string {
 | 
				
			||||||
 | 
						replaceChannelMentionFunc := func(match string) string {
 | 
				
			||||||
 | 
							channelID := match[2 : len(match)-1]
 | 
				
			||||||
 | 
							channelName := b.getChannelName(channelID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If we don't have the channel refresh our list.
 | 
				
			||||||
 | 
							if channelName == "" {
 | 
				
			||||||
 | 
								var err error
 | 
				
			||||||
 | 
								b.channels, err = b.c.GuildChannels(b.guildID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return "#unknownchannel"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								channelName = b.getChannelName(channelID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "#" + channelName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return channelMentionRE.ReplaceAllStringFunc(text, replaceChannelMentionFunc)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) replaceUserMentions(text string) string {
 | 
				
			||||||
 | 
						replaceUserMentionFunc := func(match string) string {
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								err      error
 | 
				
			||||||
 | 
								member   *discordgo.Member
 | 
				
			||||||
 | 
								username string
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							usernames := enumerateUsernames(match[1:])
 | 
				
			||||||
 | 
							for _, username = range usernames {
 | 
				
			||||||
 | 
								b.Log.Debugf("Testing mention: '%s'", username)
 | 
				
			||||||
 | 
								member, err = b.getGuildMemberByNick(username)
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if member == nil {
 | 
				
			||||||
 | 
								return match
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return strings.Replace(match, "@"+username, member.User.Mention(), 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func replaceEmotes(text string) string {
 | 
				
			||||||
 | 
						return emoteRE.ReplaceAllString(text, "$1")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) replaceAction(text string) (string, bool) {
 | 
				
			||||||
 | 
						length := len(text)
 | 
				
			||||||
 | 
						if length > 1 && text[0] == '_' && text[length-1] == '_' {
 | 
				
			||||||
 | 
							return text[1 : length-1], true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return text, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// splitURL splits a webhookURL and returns the ID and token.
 | 
				
			||||||
 | 
					func (b *Bdiscord) splitURL(url string) (string, string, bool) {
 | 
				
			||||||
 | 
						const (
 | 
				
			||||||
 | 
							expectedWebhookSplitCount = 7
 | 
				
			||||||
 | 
							webhookIdxID              = 5
 | 
				
			||||||
 | 
							webhookIdxToken           = 6
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						webhookURLSplit := strings.Split(url, "/")
 | 
				
			||||||
 | 
						if len(webhookURLSplit) != expectedWebhookSplitCount {
 | 
				
			||||||
 | 
							return "", "", false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func enumerateUsernames(s string) []string {
 | 
				
			||||||
 | 
						onlySpace := true
 | 
				
			||||||
 | 
						for _, r := range s {
 | 
				
			||||||
 | 
							if !unicode.IsSpace(r) {
 | 
				
			||||||
 | 
								onlySpace = false
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if onlySpace {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var username, endSpace string
 | 
				
			||||||
 | 
						var usernames []string
 | 
				
			||||||
 | 
						skippingSpace := true
 | 
				
			||||||
 | 
						for _, r := range s {
 | 
				
			||||||
 | 
							if unicode.IsSpace(r) {
 | 
				
			||||||
 | 
								if !skippingSpace {
 | 
				
			||||||
 | 
									usernames = append(usernames, username)
 | 
				
			||||||
 | 
									skippingSpace = true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								endSpace += string(r)
 | 
				
			||||||
 | 
								username += string(r)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								endSpace = ""
 | 
				
			||||||
 | 
								username += string(r)
 | 
				
			||||||
 | 
								skippingSpace = false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if endSpace == "" {
 | 
				
			||||||
 | 
							usernames = append(usernames, username)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return usernames
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								bridge/discord/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								bridge/discord/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package bdiscord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestEnumerateUsernames(t *testing.T) {
 | 
				
			||||||
 | 
						testcases := map[string]struct {
 | 
				
			||||||
 | 
							match             string
 | 
				
			||||||
 | 
							expectedUsernames []string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							"only space": {
 | 
				
			||||||
 | 
								match:             "  \t\n \t",
 | 
				
			||||||
 | 
								expectedUsernames: nil,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"single word": {
 | 
				
			||||||
 | 
								match:             "veni",
 | 
				
			||||||
 | 
								expectedUsernames: []string{"veni"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"single word with preceeding space": {
 | 
				
			||||||
 | 
								match:             " vidi",
 | 
				
			||||||
 | 
								expectedUsernames: []string{" vidi"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"single word with suffixed space": {
 | 
				
			||||||
 | 
								match:             "vici ",
 | 
				
			||||||
 | 
								expectedUsernames: []string{"vici"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"multi-word with varying whitespace": {
 | 
				
			||||||
 | 
								match: "just me  and\tmy friends \t",
 | 
				
			||||||
 | 
								expectedUsernames: []string{
 | 
				
			||||||
 | 
									"just",
 | 
				
			||||||
 | 
									"just me",
 | 
				
			||||||
 | 
									"just me  and",
 | 
				
			||||||
 | 
									"just me  and\tmy",
 | 
				
			||||||
 | 
									"just me  and\tmy friends",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for testname, testcase := range testcases {
 | 
				
			||||||
 | 
							foundUsernames := enumerateUsernames(testcase.match)
 | 
				
			||||||
 | 
							assert.Equalf(t, testcase.expectedUsernames, foundUsernames, "Should have found the expected usernames for testcase %s", testname)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										257
									
								
								bridge/discord/transmitter/transmitter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								bridge/discord/transmitter/transmitter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,257 @@
 | 
				
			|||||||
 | 
					// Package transmitter provides functionality for transmitting
 | 
				
			||||||
 | 
					// arbitrary webhook messages to Discord.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The package provides the following functionality:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// - Creating new webhooks, whenever necessary
 | 
				
			||||||
 | 
					// - Loading webhooks that we have previously created
 | 
				
			||||||
 | 
					// - Sending new messages
 | 
				
			||||||
 | 
					// - Editing messages, via message ID
 | 
				
			||||||
 | 
					// - Deleting messages, via message ID
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The package has been designed for matterbridge, but with other
 | 
				
			||||||
 | 
					// Go bots in mind. The public API should be matterbridge-agnostic.
 | 
				
			||||||
 | 
					package transmitter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
						log "github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A Transmitter represents a message manager for a single guild.
 | 
				
			||||||
 | 
					type Transmitter struct {
 | 
				
			||||||
 | 
						session    *discordgo.Session
 | 
				
			||||||
 | 
						guild      string
 | 
				
			||||||
 | 
						title      string
 | 
				
			||||||
 | 
						autoCreate bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// channelWebhooks maps from a channel ID to a webhook instance
 | 
				
			||||||
 | 
						channelWebhooks map[string]*discordgo.Webhook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mutex sync.RWMutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Log *log.Entry
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist
 | 
				
			||||||
 | 
					var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ErrPermissionDenied is returned if the bot does not have permission to manage webhooks.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks.
 | 
				
			||||||
 | 
					// Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks.
 | 
				
			||||||
 | 
					var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New returns a new Transmitter given a Discord session, guild ID, and title.
 | 
				
			||||||
 | 
					func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter {
 | 
				
			||||||
 | 
						return &Transmitter{
 | 
				
			||||||
 | 
							session:    session,
 | 
				
			||||||
 | 
							guild:      guild,
 | 
				
			||||||
 | 
							title:      title,
 | 
				
			||||||
 | 
							autoCreate: autoCreate,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							channelWebhooks: make(map[string]*discordgo.Webhook),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Log: log.NewEntry(log.StandardLogger()),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data.
 | 
				
			||||||
 | 
					func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) {
 | 
				
			||||||
 | 
						wh, err := t.getOrCreateWebhook(channelID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("execute failed: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return msg, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Edit will edit a message in a channel, if possible.
 | 
				
			||||||
 | 
					func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error {
 | 
				
			||||||
 | 
						wh := t.getWebhook(channelID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if wh == nil {
 | 
				
			||||||
 | 
							return ErrWebhookNotFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID
 | 
				
			||||||
 | 
						_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", ""))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HasWebhook checks whether the transmitter is using a particular webhook.
 | 
				
			||||||
 | 
					func (t *Transmitter) HasWebhook(id string) bool {
 | 
				
			||||||
 | 
						t.mutex.RLock()
 | 
				
			||||||
 | 
						defer t.mutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, wh := range t.channelWebhooks {
 | 
				
			||||||
 | 
							if wh.ID == id {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AddWebhook allows you to register a channel's webhook with the transmitter.
 | 
				
			||||||
 | 
					func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool {
 | 
				
			||||||
 | 
						t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID)
 | 
				
			||||||
 | 
						t.mutex.Lock()
 | 
				
			||||||
 | 
						defer t.mutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, replaced := t.channelWebhooks[channelID]
 | 
				
			||||||
 | 
						t.channelWebhooks[channelID] = webhook
 | 
				
			||||||
 | 
						return replaced
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Notes:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID.
 | 
				
			||||||
 | 
					// - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information.
 | 
				
			||||||
 | 
					// - This function is additive and will not unload previously loaded webhooks.
 | 
				
			||||||
 | 
					// - A nil channelIDs slice is treated the same as an empty one.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// If the bot has guild-wide permission:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 1. it will load any "relevant" webhooks from the entire guild
 | 
				
			||||||
 | 
					// 2. the given slice is ignored
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// If the bot does not have guild-wide permission:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 1. it will load any "relevant" webhooks in each channel
 | 
				
			||||||
 | 
					// 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels)
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// If any channel has more than one "relevant" webhook, it will randomly pick one.
 | 
				
			||||||
 | 
					func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error {
 | 
				
			||||||
 | 
						t.Log.Debugln("Refreshing guild webhooks")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						botID, err := getDiscordUserID(t.session)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("could not get current user: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get all existing webhooks
 | 
				
			||||||
 | 
						hooks, err := t.session.GuildWebhooks(t.guild)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							switch {
 | 
				
			||||||
 | 
							case isDiscordPermissionError(err):
 | 
				
			||||||
 | 
								// We fallback on manually fetching hooks from individual channels
 | 
				
			||||||
 | 
								// if we don't have the "Manage Webhooks" permission globally.
 | 
				
			||||||
 | 
								// We can only do this if we were provided channelIDs, though.
 | 
				
			||||||
 | 
								if len(channelIDs) == 0 {
 | 
				
			||||||
 | 
									return ErrPermissionDenied
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission")
 | 
				
			||||||
 | 
								return t.fetchChannelsHooks(channelIDs, botID)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return fmt.Errorf("could not get webhooks: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Log.Debugln("Refreshing guild webhooks using global permission")
 | 
				
			||||||
 | 
						t.assignHooksByAppID(hooks, botID, false)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// createWebhook creates a webhook for a specific channel.
 | 
				
			||||||
 | 
					func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) {
 | 
				
			||||||
 | 
						t.mutex.Lock()
 | 
				
			||||||
 | 
						defer t.mutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.channelWebhooks[channel] = wh
 | 
				
			||||||
 | 
						return wh, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook {
 | 
				
			||||||
 | 
						t.mutex.RLock()
 | 
				
			||||||
 | 
						defer t.mutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return t.channelWebhooks[channel]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) {
 | 
				
			||||||
 | 
						// If we have a webhook for this channel, immediately return it
 | 
				
			||||||
 | 
						wh := t.getWebhook(channelID)
 | 
				
			||||||
 | 
						if wh != nil {
 | 
				
			||||||
 | 
							return wh, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Early exit if we don't want to automatically create one
 | 
				
			||||||
 | 
						if !t.autoCreate {
 | 
				
			||||||
 | 
							return nil, ErrWebhookNotFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Log.Infof("Creating a webhook for %s\n", channelID)
 | 
				
			||||||
 | 
						wh, err := t.createWebhook(channelID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("could not create webhook: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return wh, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks
 | 
				
			||||||
 | 
					func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error {
 | 
				
			||||||
 | 
						// For each channel, search for relevant hooks
 | 
				
			||||||
 | 
						var failedHooks []string
 | 
				
			||||||
 | 
						for _, channelID := range channelIDs {
 | 
				
			||||||
 | 
							hooks, err := t.session.ChannelWebhooks(channelID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error())
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							t.assignHooksByAppID(hooks, botID, true)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Compose an error if any hooks failed
 | 
				
			||||||
 | 
						if len(failedHooks) > 0 {
 | 
				
			||||||
 | 
							return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, ""))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) {
 | 
				
			||||||
 | 
						logLine := "Picking up webhook"
 | 
				
			||||||
 | 
						if channelTargeted {
 | 
				
			||||||
 | 
							logLine += " (channel targeted)"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.mutex.Lock()
 | 
				
			||||||
 | 
						defer t.mutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, wh := range hooks {
 | 
				
			||||||
 | 
							if wh.ApplicationID != appID {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.channelWebhooks[wh.ChannelID] = wh
 | 
				
			||||||
 | 
							t.Log.WithFields(log.Fields{
 | 
				
			||||||
 | 
								"id":      wh.ID,
 | 
				
			||||||
 | 
								"name":    wh.Name,
 | 
				
			||||||
 | 
								"channel": wh.ChannelID,
 | 
				
			||||||
 | 
							}).Println(logLine)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								bridge/discord/transmitter/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								bridge/discord/transmitter/utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					package transmitter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
 | 
				
			||||||
 | 
					func isDiscordPermissionError(err error) bool {
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						restErr, ok := err.(*discordgo.RESTError)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getDiscordUserID gets own user ID from state, and fallback on API request
 | 
				
			||||||
 | 
					func getDiscordUserID(session *discordgo.Session) (string, error) {
 | 
				
			||||||
 | 
						if user := session.State.User; user != nil {
 | 
				
			||||||
 | 
							return user.ID, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, err := session.User("@me")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return user.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										148
									
								
								bridge/discord/webhook.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								bridge/discord/webhook.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
				
			|||||||
 | 
					package bdiscord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
 | 
				
			||||||
 | 
					func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool {
 | 
				
			||||||
 | 
						if b.useAutoWebhooks {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.channelsMutex.RLock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.RUnlock()
 | 
				
			||||||
 | 
						if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
 | 
				
			||||||
 | 
							if ci.Options.WebhookURL != "" {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// maybeGetLocalAvatar checks if UseLocalAvatar contains the message's
 | 
				
			||||||
 | 
					// account or protocol, and if so, returns the Discord avatar (if exists)
 | 
				
			||||||
 | 
					func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
 | 
				
			||||||
 | 
						for _, val := range b.GetStringSlice("UseLocalAvatar") {
 | 
				
			||||||
 | 
							if msg.Protocol != val && msg.Account != val {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							member, err := b.getGuildMemberByNick(msg.Username)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return member.User.AvatarURL("")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// webhookSend send one or more message via webhook, taking care of file
 | 
				
			||||||
 | 
					// uploads (from slack, telegram or mattermost).
 | 
				
			||||||
 | 
					// Returns messageID and error.
 | 
				
			||||||
 | 
					func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							res *discordgo.Message
 | 
				
			||||||
 | 
							err error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
 | 
				
			||||||
 | 
						if msg.Avatar == "" {
 | 
				
			||||||
 | 
							msg.Avatar = b.maybeGetLocalAvatar(msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// WebhookParams can have either `Content` or `File`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We can't send empty messages.
 | 
				
			||||||
 | 
						if msg.Text != "" {
 | 
				
			||||||
 | 
							res, err = b.transmitter.Send(
 | 
				
			||||||
 | 
								channelID,
 | 
				
			||||||
 | 
								&discordgo.WebhookParams{
 | 
				
			||||||
 | 
									Content:         msg.Text,
 | 
				
			||||||
 | 
									Username:        msg.Username,
 | 
				
			||||||
 | 
									AvatarURL:       msg.Avatar,
 | 
				
			||||||
 | 
									AllowedMentions: b.getAllowedMentions(),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
								fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
								file := discordgo.File{
 | 
				
			||||||
 | 
									Name:        fi.Name,
 | 
				
			||||||
 | 
									ContentType: "",
 | 
				
			||||||
 | 
									Reader:      bytes.NewReader(*fi.Data),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								content := fi.Comment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, e2 := b.transmitter.Send(
 | 
				
			||||||
 | 
									channelID,
 | 
				
			||||||
 | 
									&discordgo.WebhookParams{
 | 
				
			||||||
 | 
										Username:        msg.Username,
 | 
				
			||||||
 | 
										AvatarURL:       msg.Avatar,
 | 
				
			||||||
 | 
										Files:           []*discordgo.File{&file},
 | 
				
			||||||
 | 
										Content:         content,
 | 
				
			||||||
 | 
										AllowedMentions: b.getAllowedMentions(),
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								if e2 != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return res, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) {
 | 
				
			||||||
 | 
						// skip events
 | 
				
			||||||
 | 
						if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// skip empty messages
 | 
				
			||||||
 | 
						if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
 | 
				
			||||||
 | 
							b.Log.Debugf("Skipping empty message %#v", msg)
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
						msg.Text = b.replaceUserMentions(msg.Text)
 | 
				
			||||||
 | 
						// discord username must be [0..32] max
 | 
				
			||||||
 | 
						if len(msg.Username) > 32 {
 | 
				
			||||||
 | 
							msg.Username = msg.Username[0:32]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Editing webhook message")
 | 
				
			||||||
 | 
							err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{
 | 
				
			||||||
 | 
								Content:         msg.Text,
 | 
				
			||||||
 | 
								Username:        msg.Username,
 | 
				
			||||||
 | 
								AllowedMentions: b.getAllowedMentions(),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return msg.ID, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Log.Errorf("Could not edit webhook message: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Processing webhook sending for message %#v", msg)
 | 
				
			||||||
 | 
						discordMsg, err := b.webhookSend(msg, channelID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err)
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if discordMsg == nil {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return discordMsg.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										182
									
								
								bridge/gitter/gitter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								bridge/gitter/gitter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,182 @@
 | 
				
			|||||||
 | 
					package bgitter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/go-gitter"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bgitter struct {
 | 
				
			||||||
 | 
						c     *gitter.Gitter
 | 
				
			||||||
 | 
						User  *gitter.User
 | 
				
			||||||
 | 
						Users []gitter.User
 | 
				
			||||||
 | 
						Rooms []gitter.Room
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						return &Bgitter{Config: cfg}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bgitter) Connect() error {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						b.Log.Info("Connecting")
 | 
				
			||||||
 | 
						b.c = gitter.New(b.GetString("Token"))
 | 
				
			||||||
 | 
						b.User, err = b.c.GetUser()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Rooms, err = b.c.GetRooms()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bgitter) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						roomID, err := b.c.GetRoomId(channel.Name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						room, err := b.c.GetRoom(roomID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Rooms = append(b.Rooms, *room)
 | 
				
			||||||
 | 
						user, err := b.c.GetUser()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = b.c.JoinRoom(roomID, user.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						users, _ := b.c.GetUsersInRoom(roomID)
 | 
				
			||||||
 | 
						b.Users = append(b.Users, users...)
 | 
				
			||||||
 | 
						stream := b.c.Stream(roomID)
 | 
				
			||||||
 | 
						go b.c.Listen(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func(stream *gitter.Stream, room string) {
 | 
				
			||||||
 | 
							for event := range stream.Event {
 | 
				
			||||||
 | 
								switch ev := event.Data.(type) {
 | 
				
			||||||
 | 
								case *gitter.MessageReceived:
 | 
				
			||||||
 | 
									// ignore message sent from ourselves
 | 
				
			||||||
 | 
									if ev.Message.From.ID != b.User.ID {
 | 
				
			||||||
 | 
										b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
 | 
				
			||||||
 | 
										rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
 | 
				
			||||||
 | 
											Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID,
 | 
				
			||||||
 | 
											ID: ev.Message.ID}
 | 
				
			||||||
 | 
										if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
 | 
				
			||||||
 | 
											rmsg.Event = config.EventUserAction
 | 
				
			||||||
 | 
											rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
										b.Remote <- rmsg
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								case *gitter.GitterConnectionClosed:
 | 
				
			||||||
 | 
									b.Log.Errorf("connection with gitter closed for room %s", room)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}(stream, room.URI)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bgitter) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
						roomID := b.getRoomID(msg.Channel)
 | 
				
			||||||
 | 
						if roomID == "" {
 | 
				
			||||||
 | 
							b.Log.Errorf("Could not find roomID for %v", msg.Channel)
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// gitter has no delete message api so we edit message to ""
 | 
				
			||||||
 | 
							_, err := b.c.UpdateMessage(roomID, msg.ID, "")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file (in gitter case send the upload URL because gitter has no native upload support)
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return b.handleUploadFile(&msg, roomID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("updating message with id %s", msg.ID)
 | 
				
			||||||
 | 
							_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post normal message
 | 
				
			||||||
 | 
						resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return resp.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bgitter) getRoomID(channel string) string {
 | 
				
			||||||
 | 
						for _, v := range b.Rooms {
 | 
				
			||||||
 | 
							if v.URI == channel {
 | 
				
			||||||
 | 
								return v.ID
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bgitter) getAvatar(user string) string {
 | 
				
			||||||
 | 
						var avatar string
 | 
				
			||||||
 | 
						if b.Users != nil {
 | 
				
			||||||
 | 
							for _, u := range b.Users {
 | 
				
			||||||
 | 
								if user == u.Username {
 | 
				
			||||||
 | 
									return u.AvatarURLSmall
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return avatar
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (string, error) {
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								msg.Text += fi.Comment + ": "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fi.URL != "" {
 | 
				
			||||||
 | 
								msg.Text = fi.URL
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									msg.Text = fi.Comment + ": " + fi.URL
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										252
									
								
								bridge/harmony/harmony.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								bridge/harmony/harmony.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,252 @@
 | 
				
			|||||||
 | 
					package harmony
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/harmony-development/shibshib"
 | 
				
			||||||
 | 
						chatv1 "github.com/harmony-development/shibshib/gen/chat/v1"
 | 
				
			||||||
 | 
						typesv1 "github.com/harmony-development/shibshib/gen/harmonytypes/v1"
 | 
				
			||||||
 | 
						profilev1 "github.com/harmony-development/shibshib/gen/profile/v1"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type cachedProfile struct {
 | 
				
			||||||
 | 
						data        *profilev1.GetProfileResponse
 | 
				
			||||||
 | 
						lastUpdated time.Time
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bharmony struct {
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c            *shibshib.Client
 | 
				
			||||||
 | 
						profileCache map[uint64]cachedProfile
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func uToStr(in uint64) string {
 | 
				
			||||||
 | 
						return strconv.FormatUint(in, 10)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func strToU(in string) (uint64, error) {
 | 
				
			||||||
 | 
						return strconv.ParseUint(in, 10, 64)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &Bharmony{
 | 
				
			||||||
 | 
							Config:       cfg,
 | 
				
			||||||
 | 
							profileCache: map[uint64]cachedProfile{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) getProfile(u uint64) (*profilev1.GetProfileResponse, error) {
 | 
				
			||||||
 | 
						if v, ok := b.profileCache[u]; ok && time.Since(v.lastUpdated) < time.Minute*10 {
 | 
				
			||||||
 | 
							return v.data, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := b.c.ProfileKit.GetProfile(&profilev1.GetProfileRequest{
 | 
				
			||||||
 | 
							UserId: u,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if v, ok := b.profileCache[u]; ok {
 | 
				
			||||||
 | 
								return v.data, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.profileCache[u] = cachedProfile{
 | 
				
			||||||
 | 
							data:        resp,
 | 
				
			||||||
 | 
							lastUpdated: time.Now(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return resp, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) avatarFor(m *chatv1.Message) string {
 | 
				
			||||||
 | 
						if m.Overrides != nil {
 | 
				
			||||||
 | 
							return m.Overrides.GetAvatar()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						profi, err := b.getProfile(m.AuthorId)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.c.TransformHMCURL(profi.Profile.GetUserAvatar())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) usernameFor(m *chatv1.Message) string {
 | 
				
			||||||
 | 
						if m.Overrides != nil {
 | 
				
			||||||
 | 
							return m.Overrides.GetUsername()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						profi, err := b.getProfile(m.AuthorId)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return profi.Profile.UserName
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) toMessage(msg *shibshib.LocatedMessage) config.Message {
 | 
				
			||||||
 | 
						message := config.Message{}
 | 
				
			||||||
 | 
						message.Account = b.Account
 | 
				
			||||||
 | 
						message.UserID = uToStr(msg.Message.AuthorId)
 | 
				
			||||||
 | 
						message.Avatar = b.avatarFor(msg.Message)
 | 
				
			||||||
 | 
						message.Username = b.usernameFor(msg.Message)
 | 
				
			||||||
 | 
						message.Channel = uToStr(msg.ChannelID)
 | 
				
			||||||
 | 
						message.ID = uToStr(msg.MessageId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch content := msg.Message.Content.Content.(type) {
 | 
				
			||||||
 | 
						case *chatv1.Content_EmbedMessage:
 | 
				
			||||||
 | 
							message.Text = "Embed"
 | 
				
			||||||
 | 
						case *chatv1.Content_AttachmentMessage:
 | 
				
			||||||
 | 
							var s strings.Builder
 | 
				
			||||||
 | 
							for idx, attach := range content.AttachmentMessage.Files {
 | 
				
			||||||
 | 
								s.WriteString(b.c.TransformHMCURL(attach.Id))
 | 
				
			||||||
 | 
								if idx < len(content.AttachmentMessage.Files)-1 {
 | 
				
			||||||
 | 
									s.WriteString(", ")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message.Text = s.String()
 | 
				
			||||||
 | 
						case *chatv1.Content_PhotoMessage:
 | 
				
			||||||
 | 
							var s strings.Builder
 | 
				
			||||||
 | 
							for idx, attach := range content.PhotoMessage.GetPhotos() {
 | 
				
			||||||
 | 
								s.WriteString(attach.GetCaption().GetText())
 | 
				
			||||||
 | 
								s.WriteString("\n")
 | 
				
			||||||
 | 
								s.WriteString(b.c.TransformHMCURL(attach.GetHmc()))
 | 
				
			||||||
 | 
								if idx < len(content.PhotoMessage.GetPhotos())-1 {
 | 
				
			||||||
 | 
									s.WriteString("\n\n")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message.Text = s.String()
 | 
				
			||||||
 | 
						case *chatv1.Content_TextMessage:
 | 
				
			||||||
 | 
							message.Text = content.TextMessage.Content.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return message
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) outputMessages() {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							msg := <-b.c.EventsStream()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if msg.Message.AuthorId == b.c.UserID {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Remote <- b.toMessage(msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) GetUint64(conf string) uint64 {
 | 
				
			||||||
 | 
						num, err := strToU(b.GetString(conf))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return num
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) Connect() (err error) {
 | 
				
			||||||
 | 
						b.c, err = shibshib.NewClient(b.GetString("Homeserver"), b.GetString("Token"), b.GetUint64("UserID"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.c.SubscribeToGuild(b.GetUint64("Community"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go b.outputMessages()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) send(msg config.Message) (id string, err error) {
 | 
				
			||||||
 | 
						msgChan, err := strToU(msg.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						retID, err := b.c.ChatKit.SendMessage(&chatv1.SendMessageRequest{
 | 
				
			||||||
 | 
							GuildId:   b.GetUint64("Community"),
 | 
				
			||||||
 | 
							ChannelId: msgChan,
 | 
				
			||||||
 | 
							Content: &chatv1.Content{
 | 
				
			||||||
 | 
								Content: &chatv1.Content_TextMessage{
 | 
				
			||||||
 | 
									TextMessage: &chatv1.Content_TextContent{
 | 
				
			||||||
 | 
										Content: &chatv1.FormattedText{
 | 
				
			||||||
 | 
											Text: msg.Text,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Overrides: &chatv1.Overrides{
 | 
				
			||||||
 | 
								Username: &msg.Username,
 | 
				
			||||||
 | 
								Avatar:   &msg.Avatar,
 | 
				
			||||||
 | 
								Reason:   &chatv1.Overrides_Bridge{Bridge: &typesv1.Empty{}},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							InReplyTo: nil,
 | 
				
			||||||
 | 
							EchoId:    nil,
 | 
				
			||||||
 | 
							Metadata:  nil,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err = fmt.Errorf("send: error sending message: %w", err)
 | 
				
			||||||
 | 
							log.Println(err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return uToStr(retID.MessageId), err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) delete(msg config.Message) (id string, err error) {
 | 
				
			||||||
 | 
						msgChan, err := strToU(msg.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msgID, err := strToU(msg.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = b.c.ChatKit.DeleteMessage(&chatv1.DeleteMessageRequest{
 | 
				
			||||||
 | 
							GuildId:   b.GetUint64("Community"),
 | 
				
			||||||
 | 
							ChannelId: msgChan,
 | 
				
			||||||
 | 
							MessageId: msgID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return "", err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) typing(msg config.Message) (id string, err error) {
 | 
				
			||||||
 | 
						msgChan, err := strToU(msg.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = b.c.ChatKit.Typing(&chatv1.TypingRequest{
 | 
				
			||||||
 | 
							GuildId:   b.GetUint64("Community"),
 | 
				
			||||||
 | 
							ChannelId: msgChan,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return "", err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) Send(msg config.Message) (id string, err error) {
 | 
				
			||||||
 | 
						switch msg.Event {
 | 
				
			||||||
 | 
						case "":
 | 
				
			||||||
 | 
							return b.send(msg)
 | 
				
			||||||
 | 
						case config.EventMsgDelete:
 | 
				
			||||||
 | 
							return b.delete(msg)
 | 
				
			||||||
 | 
						case config.EventUserTyping:
 | 
				
			||||||
 | 
							return b.typing(msg)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bharmony) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										244
									
								
								bridge/helper/helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								bridge/helper/helper.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,244 @@
 | 
				
			|||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"image/png"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
						"unicode/utf8"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/image/webp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/gomarkdown/markdown"
 | 
				
			||||||
 | 
						"github.com/gomarkdown/markdown/html"
 | 
				
			||||||
 | 
						"github.com/gomarkdown/markdown/parser"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DownloadFile downloads the given non-authenticated URL.
 | 
				
			||||||
 | 
					func DownloadFile(url string) (*[]byte, error) {
 | 
				
			||||||
 | 
						return DownloadFileAuth(url, "")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DownloadFileAuth downloads the given URL using the specified authentication token.
 | 
				
			||||||
 | 
					func DownloadFileAuth(url string, auth string) (*[]byte, error) {
 | 
				
			||||||
 | 
						var buf bytes.Buffer
 | 
				
			||||||
 | 
						client := &http.Client{
 | 
				
			||||||
 | 
							Timeout: time.Second * 5,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req, err := http.NewRequest("GET", url, nil)
 | 
				
			||||||
 | 
						if auth != "" {
 | 
				
			||||||
 | 
							req.Header.Add("Authorization", auth)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resp, err := client.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						io.Copy(&buf, resp.Body)
 | 
				
			||||||
 | 
						data := buf.Bytes()
 | 
				
			||||||
 | 
						return &data, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DownloadFileAuthRocket downloads the given URL using the specified Rocket user ID and authentication token.
 | 
				
			||||||
 | 
					func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) {
 | 
				
			||||||
 | 
						var buf bytes.Buffer
 | 
				
			||||||
 | 
						client := &http.Client{
 | 
				
			||||||
 | 
							Timeout: time.Second * 5,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req, err := http.NewRequest("GET", url, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req.Header.Add("X-Auth-Token", token)
 | 
				
			||||||
 | 
						req.Header.Add("X-User-Id", userID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resp, err := client.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						_, err = io.Copy(&buf, resp.Body)
 | 
				
			||||||
 | 
						data := buf.Bytes()
 | 
				
			||||||
 | 
						return &data, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetSubLines splits messages in newline-delimited lines. If maxLineLength is
 | 
				
			||||||
 | 
					// specified as non-zero GetSubLines will also clip long lines to the maximum
 | 
				
			||||||
 | 
					// length and insert a warning marker that the line was clipped.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// TODO: The current implementation has the inconvenient that it disregards
 | 
				
			||||||
 | 
					// word boundaries when splitting but this is hard to solve without potentially
 | 
				
			||||||
 | 
					// breaking formatting and other stylistic effects.
 | 
				
			||||||
 | 
					func GetSubLines(message string, maxLineLength int, clippingMessage string) []string {
 | 
				
			||||||
 | 
						if clippingMessage == "" {
 | 
				
			||||||
 | 
							clippingMessage = " <clipped message>"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var lines []string
 | 
				
			||||||
 | 
						for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
 | 
				
			||||||
 | 
							if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
 | 
				
			||||||
 | 
								lines = append(lines, line)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// !!! WARNING !!!
 | 
				
			||||||
 | 
							// Before touching the splitting logic below please ensure that you PROPERLY
 | 
				
			||||||
 | 
							// understand how strings, runes and range loops over strings work in Go.
 | 
				
			||||||
 | 
							// A good place to start is to read https://blog.golang.org/strings. :-)
 | 
				
			||||||
 | 
							var splitStart int
 | 
				
			||||||
 | 
							var startOfPreviousRune int
 | 
				
			||||||
 | 
							for i := range line {
 | 
				
			||||||
 | 
								if i-splitStart > maxLineLength-len([]byte(clippingMessage)) {
 | 
				
			||||||
 | 
									lines = append(lines, line[splitStart:startOfPreviousRune]+clippingMessage)
 | 
				
			||||||
 | 
									splitStart = startOfPreviousRune
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								startOfPreviousRune = i
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// This last append is safe to do without looking at the remaining byte-length
 | 
				
			||||||
 | 
							// as we assume that the byte-length of the last rune will never exceed that of
 | 
				
			||||||
 | 
							// the byte-length of the clipping message.
 | 
				
			||||||
 | 
							lines = append(lines, line[splitStart:])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lines
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleExtra manages the supplementary details stored inside a message's 'Extra' field map.
 | 
				
			||||||
 | 
					func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message {
 | 
				
			||||||
 | 
						extra := msg.Extra
 | 
				
			||||||
 | 
						rmsg := []config.Message{}
 | 
				
			||||||
 | 
						for _, f := range extra[config.EventFileFailureSize] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize)
 | 
				
			||||||
 | 
							rmsg = append(rmsg, config.Message{
 | 
				
			||||||
 | 
								Text:     text,
 | 
				
			||||||
 | 
								Username: "<system> ",
 | 
				
			||||||
 | 
								Channel:  msg.Channel,
 | 
				
			||||||
 | 
								Account:  msg.Account,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetAvatar constructs a URL for a given user-avatar if it is available in the cache.
 | 
				
			||||||
 | 
					func GetAvatar(av map[string]string, userid string, general *config.Protocol) string {
 | 
				
			||||||
 | 
						if sha, ok := av[userid]; ok {
 | 
				
			||||||
 | 
							return general.MediaServerDownload + "/" + sha + "/" + userid + ".png"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleDownloadSize checks a specified filename against the configured download blacklist
 | 
				
			||||||
 | 
					// and checks a specified file-size against the configure limit.
 | 
				
			||||||
 | 
					func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string, size int64, general *config.Protocol) error {
 | 
				
			||||||
 | 
						// check blacklist here
 | 
				
			||||||
 | 
						for _, entry := range general.MediaDownloadBlackList {
 | 
				
			||||||
 | 
							if entry != "" {
 | 
				
			||||||
 | 
								re, err := regexp.Compile(entry)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logger.Errorf("incorrect regexp %s for %s", entry, msg.Account)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if re.MatchString(name) {
 | 
				
			||||||
 | 
									return fmt.Errorf("Matching blacklist %s. Not downloading %s", entry, name)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						logger.Debugf("Trying to download %#v with size %#v", name, size)
 | 
				
			||||||
 | 
						if int(size) > general.MediaDownloadSize {
 | 
				
			||||||
 | 
							msg.Event = config.EventFileFailureSize
 | 
				
			||||||
 | 
							msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{
 | 
				
			||||||
 | 
								Name:    name,
 | 
				
			||||||
 | 
								Comment: msg.Text,
 | 
				
			||||||
 | 
								Size:    size,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							return fmt.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, general.MediaDownloadSize)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
 | 
				
			||||||
 | 
					func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
 | 
				
			||||||
 | 
						HandleDownloadData2(logger, msg, name, "", comment, url, data, general)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
 | 
				
			||||||
 | 
					func HandleDownloadData2(logger *logrus.Entry, msg *config.Message, name, id, comment, url string, data *[]byte, general *config.Protocol) {
 | 
				
			||||||
 | 
						var avatar bool
 | 
				
			||||||
 | 
						logger.Debugf("Download OK %#v %#v", name, len(*data))
 | 
				
			||||||
 | 
						if msg.Event == config.EventAvatarDownload {
 | 
				
			||||||
 | 
							avatar = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
 | 
				
			||||||
 | 
							Name:     name,
 | 
				
			||||||
 | 
							Data:     data,
 | 
				
			||||||
 | 
							URL:      url,
 | 
				
			||||||
 | 
							Comment:  comment,
 | 
				
			||||||
 | 
							Avatar:   avatar,
 | 
				
			||||||
 | 
							NativeID: id,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var emptyLineMatcher = regexp.MustCompile("\n+")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveEmptyNewLines collapses consecutive newline characters into a single one and
 | 
				
			||||||
 | 
					// trims any preceding or trailing newline characters as well.
 | 
				
			||||||
 | 
					func RemoveEmptyNewLines(msg string) string {
 | 
				
			||||||
 | 
						return emptyLineMatcher.ReplaceAllString(strings.Trim(msg, "\n"), "\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ClipMessage trims a message to the specified length if it exceeds it and adds a warning
 | 
				
			||||||
 | 
					// to the message in case it does so.
 | 
				
			||||||
 | 
					func ClipMessage(text string, length int, clippingMessage string) string {
 | 
				
			||||||
 | 
						if clippingMessage == "" {
 | 
				
			||||||
 | 
							clippingMessage = " <clipped message>"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(text) > length {
 | 
				
			||||||
 | 
							text = text[:length-len(clippingMessage)]
 | 
				
			||||||
 | 
							if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
 | 
				
			||||||
 | 
								text = text[:len(text)-size]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							text += clippingMessage
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return text
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ParseMarkdown takes in an input string as markdown and parses it to html
 | 
				
			||||||
 | 
					func ParseMarkdown(input string) string {
 | 
				
			||||||
 | 
						extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode
 | 
				
			||||||
 | 
						markdownParser := parser.NewWithExtensions(extensions)
 | 
				
			||||||
 | 
						renderer := html.NewRenderer(html.RendererOptions{
 | 
				
			||||||
 | 
							Flags: 0,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						parsedMarkdown := markdown.ToHTML([]byte(input), markdownParser, renderer)
 | 
				
			||||||
 | 
						res := string(parsedMarkdown)
 | 
				
			||||||
 | 
						res = strings.TrimPrefix(res, "<p>")
 | 
				
			||||||
 | 
						res = strings.TrimSuffix(res, "</p>\n")
 | 
				
			||||||
 | 
						return res
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConvertWebPToPNG converts input data (which should be WebP format) to PNG format
 | 
				
			||||||
 | 
					func ConvertWebPToPNG(data *[]byte) error {
 | 
				
			||||||
 | 
						r := bytes.NewReader(*data)
 | 
				
			||||||
 | 
						m, err := webp.Decode(r)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var output []byte
 | 
				
			||||||
 | 
						w := bytes.NewBuffer(output)
 | 
				
			||||||
 | 
						if err := png.Encode(w, m); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						*data = w.Bytes()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										127
									
								
								bridge/helper/helper_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								bridge/helper/helper_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
				
			|||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const testLineLength = 64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var lineSplittingTestCases = map[string]struct {
 | 
				
			||||||
 | 
						input          string
 | 
				
			||||||
 | 
						splitOutput    []string
 | 
				
			||||||
 | 
						nonSplitOutput []string
 | 
				
			||||||
 | 
					}{
 | 
				
			||||||
 | 
						"Short single-line message": {
 | 
				
			||||||
 | 
							input:          "short",
 | 
				
			||||||
 | 
							splitOutput:    []string{"short"},
 | 
				
			||||||
 | 
							nonSplitOutput: []string{"short"},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"Long single-line message": {
 | 
				
			||||||
 | 
							input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
				
			||||||
 | 
							splitOutput: []string{
 | 
				
			||||||
 | 
								"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
 | 
				
			||||||
 | 
								"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
 | 
				
			||||||
 | 
								" labore et dolore magna aliqua.",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"Short multi-line message": {
 | 
				
			||||||
 | 
							input: "I\ncan't\nget\nno\nsatisfaction!",
 | 
				
			||||||
 | 
							splitOutput: []string{
 | 
				
			||||||
 | 
								"I",
 | 
				
			||||||
 | 
								"can't",
 | 
				
			||||||
 | 
								"get",
 | 
				
			||||||
 | 
								"no",
 | 
				
			||||||
 | 
								"satisfaction!",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							nonSplitOutput: []string{
 | 
				
			||||||
 | 
								"I",
 | 
				
			||||||
 | 
								"can't",
 | 
				
			||||||
 | 
								"get",
 | 
				
			||||||
 | 
								"no",
 | 
				
			||||||
 | 
								"satisfaction!",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"Long multi-line message": {
 | 
				
			||||||
 | 
							input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
 | 
				
			||||||
 | 
								"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
 | 
				
			||||||
 | 
								"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
 | 
				
			||||||
 | 
								"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
 | 
				
			||||||
 | 
							splitOutput: []string{
 | 
				
			||||||
 | 
								"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
 | 
				
			||||||
 | 
								"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
 | 
				
			||||||
 | 
								" labore et dolore magna aliqua.",
 | 
				
			||||||
 | 
								"Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
 | 
				
			||||||
 | 
								"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
 | 
				
			||||||
 | 
								"modo consequat.",
 | 
				
			||||||
 | 
								"Duis aute irure dolor in reprehenderit in volu <clipped message>",
 | 
				
			||||||
 | 
								"ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
 | 
				
			||||||
 | 
								" pariatur.",
 | 
				
			||||||
 | 
								"Excepteur sint occaecat cupidatat non proident <clipped message>",
 | 
				
			||||||
 | 
								", sunt in culpa qui officia deserunt mollit an <clipped message>",
 | 
				
			||||||
 | 
								"im id est laborum.",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							nonSplitOutput: []string{
 | 
				
			||||||
 | 
								"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
				
			||||||
 | 
								"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
 | 
				
			||||||
 | 
								"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
 | 
				
			||||||
 | 
								"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"Message ending with new-line.": {
 | 
				
			||||||
 | 
							input:          "Newline ending\n",
 | 
				
			||||||
 | 
							splitOutput:    []string{"Newline ending"},
 | 
				
			||||||
 | 
							nonSplitOutput: []string{"Newline ending"},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"Long message containing UTF-8 multi-byte runes": {
 | 
				
			||||||
 | 
							input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
 | 
				
			||||||
 | 
							splitOutput: []string{
 | 
				
			||||||
 | 
								"不布人個我此而及單石業喜資富下 <clipped message>",
 | 
				
			||||||
 | 
								"我河下日沒一我臺空達的常景便物 <clipped message>",
 | 
				
			||||||
 | 
								"沒為……子大我別名解成?生賣的 <clipped message>",
 | 
				
			||||||
 | 
								"全直黑,我自我結毛分洲了世當, <clipped message>",
 | 
				
			||||||
 | 
								"是政福那是東;斯說",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetSubLines(t *testing.T) {
 | 
				
			||||||
 | 
						for testname, testcase := range lineSplittingTestCases {
 | 
				
			||||||
 | 
							splitLines := GetSubLines(testcase.input, testLineLength, "")
 | 
				
			||||||
 | 
							assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
 | 
				
			||||||
 | 
							for _, splitLine := range splitLines {
 | 
				
			||||||
 | 
								byteLength := len([]byte(splitLine))
 | 
				
			||||||
 | 
								assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							nonSplitLines := GetSubLines(testcase.input, 0, "")
 | 
				
			||||||
 | 
							assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestConvertWebPToPNG(t *testing.T) {
 | 
				
			||||||
 | 
						if os.Getenv("LOCAL_TEST") == "" {
 | 
				
			||||||
 | 
							t.Skip()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						input, err := ioutil.ReadFile("test.webp")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fail()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						d := &input
 | 
				
			||||||
 | 
						err = ConvertWebPToPNG(d)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fail()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fail()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								bridge/helper/libtgsconverter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								bridge/helper/libtgsconverter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					//go:build cgo
 | 
				
			||||||
 | 
					// +build cgo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/Benau/tgsconverter/libtgsconverter"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func CanConvertTgsToX() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConvertTgsToX convert input data (which should be tgs format) to any format supported by libtgsconverter
 | 
				
			||||||
 | 
					func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
 | 
				
			||||||
 | 
						options := libtgsconverter.NewConverterOptions()
 | 
				
			||||||
 | 
						options.SetExtension(outputFormat)
 | 
				
			||||||
 | 
						blob, err := libtgsconverter.ImportFromData(*data, options)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to run libtgsconverter.ImportFromData: %s", err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*data = blob
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SupportsFormat(format string) bool {
 | 
				
			||||||
 | 
						return libtgsconverter.SupportsExtension(format)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LottieBackend() string {
 | 
				
			||||||
 | 
						return "libtgsconverter"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								bridge/helper/lottie_convert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								bridge/helper/lottie_convert.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					// +build !cgo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works.
 | 
				
			||||||
 | 
					func CanConvertTgsToX() error {
 | 
				
			||||||
 | 
						// We depend on the fact that `lottie_convert.py --help` has exit status 0.
 | 
				
			||||||
 | 
						// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually.
 | 
				
			||||||
 | 
						// However, there is no alternative like `lottie_convert.py --is-properly-installed`
 | 
				
			||||||
 | 
						cmd := exec.Command("lottie_convert.py", "--help")
 | 
				
			||||||
 | 
						return cmd.Run()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConvertTgsToWebP convert input data (which should be tgs format) to WebP format
 | 
				
			||||||
 | 
					// This relies on an external command, which is ugly, but works.
 | 
				
			||||||
 | 
					func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
 | 
				
			||||||
 | 
						// lottie can't handle input from a pipe, so write to a temporary file:
 | 
				
			||||||
 | 
						tmpInFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-input-*.tgs")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tmpInFileName := tmpInFile.Name()
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if removeErr := os.Remove(tmpInFileName); removeErr != nil {
 | 
				
			||||||
 | 
								logger.Errorf("Could not delete temporary (input) file %s: %v", tmpInFileName, removeErr)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						// lottie can handle writing to a pipe, but there is no way to do that platform-independently.
 | 
				
			||||||
 | 
						// "/dev/stdout" won't work on Windows, and "-" upsets Cairo for some reason. So we need another file:
 | 
				
			||||||
 | 
						tmpOutFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-output-*.data")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tmpOutFileName := tmpOutFile.Name()
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if removeErr := os.Remove(tmpOutFileName); removeErr != nil {
 | 
				
			||||||
 | 
								logger.Errorf("Could not delete temporary (output) file %s: %v", tmpOutFileName, removeErr)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, writeErr := tmpInFile.Write(*data); writeErr != nil {
 | 
				
			||||||
 | 
							return writeErr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Must close before calling lottie to avoid data races:
 | 
				
			||||||
 | 
						if closeErr := tmpInFile.Close(); closeErr != nil {
 | 
				
			||||||
 | 
							return closeErr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Call lottie to transform:
 | 
				
			||||||
 | 
						cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpInFileName, tmpOutFileName)
 | 
				
			||||||
 | 
						cmd.Stdout = nil
 | 
				
			||||||
 | 
						cmd.Stderr = nil
 | 
				
			||||||
 | 
						// NB: lottie writes progress into to stderr in all cases.
 | 
				
			||||||
 | 
						_, stderr := cmd.Output()
 | 
				
			||||||
 | 
						if stderr != nil {
 | 
				
			||||||
 | 
							// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
 | 
				
			||||||
 | 
							return stderr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						dataContents, err := ioutil.ReadFile(tmpOutFileName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*data = dataContents
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SupportsFormat(format string) bool {
 | 
				
			||||||
 | 
						switch format {
 | 
				
			||||||
 | 
						case "png":
 | 
				
			||||||
 | 
							fallthrough
 | 
				
			||||||
 | 
						case "webp":
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LottieBackend() string {
 | 
				
			||||||
 | 
						return "lottie_convert.py"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								bridge/irc/charset.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								bridge/irc/charset.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					package birc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"golang.org/x/text/encoding"
 | 
				
			||||||
 | 
						"golang.org/x/text/encoding/japanese"
 | 
				
			||||||
 | 
						"golang.org/x/text/encoding/korean"
 | 
				
			||||||
 | 
						"golang.org/x/text/encoding/simplifiedchinese"
 | 
				
			||||||
 | 
						"golang.org/x/text/encoding/traditionalchinese"
 | 
				
			||||||
 | 
						"golang.org/x/text/encoding/unicode"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var encoders = map[string]encoding.Encoding{
 | 
				
			||||||
 | 
						"utf-8":       unicode.UTF8,
 | 
				
			||||||
 | 
						"iso-2022-jp": japanese.ISO2022JP,
 | 
				
			||||||
 | 
						"big5":        traditionalchinese.Big5,
 | 
				
			||||||
 | 
						"gbk":         simplifiedchinese.GBK,
 | 
				
			||||||
 | 
						"euc-kr":      korean.EUCKR,
 | 
				
			||||||
 | 
						"gb2312":      simplifiedchinese.HZGB2312,
 | 
				
			||||||
 | 
						"shift-jis":   japanese.ShiftJIS,
 | 
				
			||||||
 | 
						"euc-jp":      japanese.EUCJP,
 | 
				
			||||||
 | 
						"gb18030":     simplifiedchinese.GB18030,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func toUTF8(from string, input string) string {
 | 
				
			||||||
 | 
						enc, ok := encoders[from]
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return input
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res, _ := enc.NewDecoder().String(input)
 | 
				
			||||||
 | 
						return res
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										264
									
								
								bridge/irc/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								bridge/irc/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,264 @@
 | 
				
			|||||||
 | 
					package birc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/lrstanley/girc"
 | 
				
			||||||
 | 
						"github.com/paulrosania/go-charset/charset"
 | 
				
			||||||
 | 
						"github.com/saintfish/chardet"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We need to import the 'data' package as an implicit dependency.
 | 
				
			||||||
 | 
						// See: https://godoc.org/github.com/paulrosania/go-charset/charset
 | 
				
			||||||
 | 
						_ "github.com/paulrosania/go-charset/data"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleCharset(msg *config.Message) error {
 | 
				
			||||||
 | 
						if b.GetString("Charset") != "" {
 | 
				
			||||||
 | 
							switch b.GetString("Charset") {
 | 
				
			||||||
 | 
							case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
 | 
				
			||||||
 | 
								msg.Text = toUTF8(b.GetString("Charset"), msg.Text)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								buf := new(bytes.Buffer)
 | 
				
			||||||
 | 
								w, err := charset.NewWriter(b.GetString("Charset"), buf)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								fmt.Fprint(w, msg.Text)
 | 
				
			||||||
 | 
								w.Close()
 | 
				
			||||||
 | 
								msg.Text = buf.String()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleFiles returns true if we have handled the files, otherwise return false
 | 
				
			||||||
 | 
					func (b *Birc) handleFiles(msg *config.Message) bool {
 | 
				
			||||||
 | 
						if msg.Extra == nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, rmsg := range helper.HandleExtra(msg, b.General) {
 | 
				
			||||||
 | 
							b.Local <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(msg.Extra["file"]) == 0 {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								msg.Text += fi.Comment + " : "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fi.URL != "" {
 | 
				
			||||||
 | 
								msg.Text = fi.URL
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									msg.Text = fi.Comment + " : " + fi.URL
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleInvite(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						if len(event.Params) != 2 {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channel := event.Params[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("got invite for %s", channel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, ok := b.channels[channel]; ok {
 | 
				
			||||||
 | 
							b.i.Cmd.Join(channel)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						if len(event.Params) == 0 {
 | 
				
			||||||
 | 
							b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						channel := strings.ToLower(event.Params[0])
 | 
				
			||||||
 | 
						if event.Command == "KICK" && event.Params[1] == b.Nick {
 | 
				
			||||||
 | 
							b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name)
 | 
				
			||||||
 | 
							time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second)
 | 
				
			||||||
 | 
							b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if event.Command == "QUIT" {
 | 
				
			||||||
 | 
							if event.Source.Name == b.Nick && strings.Contains(event.Last(), "Ping timeout") {
 | 
				
			||||||
 | 
								b.Log.Infof("%s reconnecting ..", b.Account)
 | 
				
			||||||
 | 
								b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure}
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if event.Source.Name != b.Nick {
 | 
				
			||||||
 | 
							if b.GetBool("nosendjoinpart") {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
 | 
				
			||||||
 | 
							if b.GetBool("verbosejoinpart") {
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Sending verbose JOIN_LEAVE event from %s to gateway", b.Account)
 | 
				
			||||||
 | 
								msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Message is %#v", msg)
 | 
				
			||||||
 | 
							b.Remote <- msg
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("handle %#v", event)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						b.Log.Debug("Registering callbacks")
 | 
				
			||||||
 | 
						i := b.i
 | 
				
			||||||
 | 
						b.Nick = event.Params[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
 | 
				
			||||||
 | 
						i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg)
 | 
				
			||||||
 | 
						i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
 | 
				
			||||||
 | 
						i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
 | 
				
			||||||
 | 
						i.Handlers.AddBg("JOIN", b.handleJoinPart)
 | 
				
			||||||
 | 
						i.Handlers.AddBg("PART", b.handleJoinPart)
 | 
				
			||||||
 | 
						i.Handlers.AddBg("QUIT", b.handleJoinPart)
 | 
				
			||||||
 | 
						i.Handlers.AddBg("KICK", b.handleJoinPart)
 | 
				
			||||||
 | 
						i.Handlers.Add("INVITE", b.handleInvite)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleNickServ() {
 | 
				
			||||||
 | 
						if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick"))
 | 
				
			||||||
 | 
							b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") {
 | 
				
			||||||
 | 
							b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick"))
 | 
				
			||||||
 | 
							b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// give nickserv some slack
 | 
				
			||||||
 | 
						time.Sleep(time.Second * 5)
 | 
				
			||||||
 | 
						b.authDone = true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") {
 | 
				
			||||||
 | 
							b.handleNickServ()
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.handlePrivMsg(client, event)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						if b.GetInt("DebugLevel") == 1 {
 | 
				
			||||||
 | 
							if event.Command != "CLIENT_STATE_UPDATED" &&
 | 
				
			||||||
 | 
								event.Command != "CLIENT_GENERAL_UPDATED" {
 | 
				
			||||||
 | 
								b.Log.Debugf("%#v", event.String())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						switch event.Command {
 | 
				
			||||||
 | 
						case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("%#v", event.String())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						b.handleNickServ()
 | 
				
			||||||
 | 
						b.handleRunCommands()
 | 
				
			||||||
 | 
						// we are now fully connected
 | 
				
			||||||
 | 
						// only send on first connection
 | 
				
			||||||
 | 
						if b.FirstConnection {
 | 
				
			||||||
 | 
							b.connected <- nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						if b.skipPrivMsg(event) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							Username: event.Source.Name,
 | 
				
			||||||
 | 
							Channel:  strings.ToLower(event.Params[0]),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							UserID:   event.Source.Ident + "@" + event.Source.Host,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// set action event
 | 
				
			||||||
 | 
						if event.IsAction() {
 | 
				
			||||||
 | 
							rmsg.Event = config.EventUserAction
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// set NOTICE event
 | 
				
			||||||
 | 
						if event.Command == "NOTICE" {
 | 
				
			||||||
 | 
							rmsg.Event = config.EventNoticeIRC
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// strip action, we made an event if it was an action
 | 
				
			||||||
 | 
						rmsg.Text += event.StripAction()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// start detecting the charset
 | 
				
			||||||
 | 
						mycharset := b.GetString("Charset")
 | 
				
			||||||
 | 
						if mycharset == "" {
 | 
				
			||||||
 | 
							// detect what were sending so that we convert it to utf-8
 | 
				
			||||||
 | 
							detector := chardet.NewTextDetector()
 | 
				
			||||||
 | 
							result, err := detector.DetectBest([]byte(rmsg.Text))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
 | 
				
			||||||
 | 
							mycharset = result.Charset
 | 
				
			||||||
 | 
							// if we're not sure, just pick ISO-8859-1
 | 
				
			||||||
 | 
							if result.Confidence < 80 {
 | 
				
			||||||
 | 
								mycharset = "ISO-8859-1"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						switch mycharset {
 | 
				
			||||||
 | 
						case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
 | 
				
			||||||
 | 
							rmsg.Text = toUTF8(b.GetString("Charset"), rmsg.Text)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							output, _ := ioutil.ReadAll(r)
 | 
				
			||||||
 | 
							rmsg.Text = string(output)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account)
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleRunCommands() {
 | 
				
			||||||
 | 
						for _, cmd := range b.GetStringSlice("RunCommands") {
 | 
				
			||||||
 | 
							if err := b.i.Cmd.SendRaw(cmd); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							time.Sleep(time.Second)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						parts := strings.Split(event.Params[2], "!")
 | 
				
			||||||
 | 
						t, err := strconv.ParseInt(event.Params[3], 10, 64)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Invalid time stamp: %s", event.Params[3])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := parts[0]
 | 
				
			||||||
 | 
						if len(parts) > 1 {
 | 
				
			||||||
 | 
							user += " [" + parts[1] + "]"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										415
									
								
								bridge/irc/irc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										415
									
								
								bridge/irc/irc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,415 @@
 | 
				
			|||||||
 | 
					package birc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"hash/crc32"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/lrstanley/girc"
 | 
				
			||||||
 | 
						stripmd "github.com/writeas/go-strip-markdown"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We need to import the 'data' package as an implicit dependency.
 | 
				
			||||||
 | 
						// See: https://godoc.org/github.com/paulrosania/go-charset/charset
 | 
				
			||||||
 | 
						_ "github.com/paulrosania/go-charset/data"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Birc struct {
 | 
				
			||||||
 | 
						i                                         *girc.Client
 | 
				
			||||||
 | 
						Nick                                      string
 | 
				
			||||||
 | 
						names                                     map[string][]string
 | 
				
			||||||
 | 
						connected                                 chan error
 | 
				
			||||||
 | 
						Local                                     chan config.Message // local queue for flood control
 | 
				
			||||||
 | 
						FirstConnection, authDone                 bool
 | 
				
			||||||
 | 
						MessageDelay, MessageQueue, MessageLength int
 | 
				
			||||||
 | 
						channels                                  map[string]bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &Birc{}
 | 
				
			||||||
 | 
						b.Config = cfg
 | 
				
			||||||
 | 
						b.Nick = b.GetString("Nick")
 | 
				
			||||||
 | 
						b.names = make(map[string][]string)
 | 
				
			||||||
 | 
						b.connected = make(chan error)
 | 
				
			||||||
 | 
						b.channels = make(map[string]bool)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetInt("MessageDelay") == 0 {
 | 
				
			||||||
 | 
							b.MessageDelay = 1300
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.MessageDelay = b.GetInt("MessageDelay")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetInt("MessageQueue") == 0 {
 | 
				
			||||||
 | 
							b.MessageQueue = 30
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.MessageQueue = b.GetInt("MessageQueue")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetInt("MessageLength") == 0 {
 | 
				
			||||||
 | 
							b.MessageLength = 400
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.MessageLength = b.GetInt("MessageLength")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.FirstConnection = true
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) Command(msg *config.Message) string {
 | 
				
			||||||
 | 
						if msg.Text == "!users" {
 | 
				
			||||||
 | 
							b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
 | 
				
			||||||
 | 
							b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
 | 
				
			||||||
 | 
							b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) Connect() error {
 | 
				
			||||||
 | 
						if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" {
 | 
				
			||||||
 | 
							return errors.New("you can't enable SASL and TLSClientCertificate at the same time")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Local = make(chan config.Message, b.MessageQueue+10)
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						i, err := b.getClient()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("UseSASL") {
 | 
				
			||||||
 | 
							i.Config.SASL = &girc.SASLPlain{
 | 
				
			||||||
 | 
								User: b.GetString("NickServNick"),
 | 
				
			||||||
 | 
								Pass: b.GetString("NickServPassword"),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
 | 
				
			||||||
 | 
						i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
 | 
				
			||||||
 | 
						i.Handlers.Add(girc.ERR_NOMOTD, b.handleOtherAuth)
 | 
				
			||||||
 | 
						i.Handlers.Add(girc.ALL_EVENTS, b.handleOther)
 | 
				
			||||||
 | 
						b.i = i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go b.doConnect()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = <-b.connected
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("connection failed %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						b.FirstConnection = false
 | 
				
			||||||
 | 
						if b.GetInt("DebugLevel") == 0 {
 | 
				
			||||||
 | 
							i.Handlers.Clear(girc.ALL_EVENTS)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						go b.doSend()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) Disconnect() error {
 | 
				
			||||||
 | 
						b.i.Close()
 | 
				
			||||||
 | 
						close(b.Local)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						b.channels[channel.Name] = true
 | 
				
			||||||
 | 
						// need to check if we have nickserv auth done before joining channels
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							if b.authDone {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							time.Sleep(time.Second)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if channel.Options.Key != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
 | 
				
			||||||
 | 
							b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.i.Cmd.Join(channel.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// ignore delete messages
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we can be in between reconnects #385
 | 
				
			||||||
 | 
						if !b.i.IsConnected() {
 | 
				
			||||||
 | 
							b.Log.Error("Not connected to server, dropping message")
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Execute a command
 | 
				
			||||||
 | 
						if strings.HasPrefix(msg.Text, "!") {
 | 
				
			||||||
 | 
							b.Command(&msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// convert to specified charset
 | 
				
			||||||
 | 
						if err := b.handleCharset(&msg); err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// handle files, return if we're done here
 | 
				
			||||||
 | 
						if ok := b.handleFiles(&msg); ok {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var msgLines []string
 | 
				
			||||||
 | 
						if b.GetBool("StripMarkdown") {
 | 
				
			||||||
 | 
							msg.Text = stripmd.Strip(msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("MessageSplit") {
 | 
				
			||||||
 | 
							msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i := range msgLines {
 | 
				
			||||||
 | 
							if len(b.Local) >= b.MessageQueue {
 | 
				
			||||||
 | 
								b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							msg.Text = msgLines[i]
 | 
				
			||||||
 | 
							b.Local <- msg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) doConnect() {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							if err := b.i.Connect(); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("disconnect: error: %s", err)
 | 
				
			||||||
 | 
								if b.FirstConnection {
 | 
				
			||||||
 | 
									b.connected <- err
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								b.Log.Info("disconnect: client requested quit")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Log.Info("reconnecting in 30 seconds...")
 | 
				
			||||||
 | 
							time.Sleep(30 * time.Second)
 | 
				
			||||||
 | 
							b.i.Handlers.Clear(girc.RPL_WELCOME)
 | 
				
			||||||
 | 
							b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
								b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
 | 
				
			||||||
 | 
								// set our correct nick on reconnect if necessary
 | 
				
			||||||
 | 
								b.Nick = event.Source.Name
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-"
 | 
				
			||||||
 | 
					func sanitizeNick(nick string) string {
 | 
				
			||||||
 | 
						sanitize := func(r rune) rune {
 | 
				
			||||||
 | 
							if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) {
 | 
				
			||||||
 | 
								return '-'
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return r
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return strings.Map(sanitize, nick)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) doSend() {
 | 
				
			||||||
 | 
						rate := time.Millisecond * time.Duration(b.MessageDelay)
 | 
				
			||||||
 | 
						throttle := time.NewTicker(rate)
 | 
				
			||||||
 | 
						for msg := range b.Local {
 | 
				
			||||||
 | 
							<-throttle.C
 | 
				
			||||||
 | 
							username := msg.Username
 | 
				
			||||||
 | 
							// Optional support for the proposed RELAYMSG extension, described at
 | 
				
			||||||
 | 
							// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
 | 
				
			||||||
 | 
							// nolint:nestif
 | 
				
			||||||
 | 
							if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
 | 
				
			||||||
 | 
								b.GetBool("UseRelayMsg") {
 | 
				
			||||||
 | 
								username = sanitizeNick(username)
 | 
				
			||||||
 | 
								text := msg.Text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Work around girc chomping leading commas on single word messages?
 | 
				
			||||||
 | 
								if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
 | 
				
			||||||
 | 
									text = ":" + text
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
									b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
 | 
				
			||||||
 | 
									b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								if b.GetBool("Colornicks") {
 | 
				
			||||||
 | 
									checksum := crc32.ChecksumIEEE([]byte(msg.Username))
 | 
				
			||||||
 | 
									colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
 | 
				
			||||||
 | 
									username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								switch msg.Event {
 | 
				
			||||||
 | 
								case config.EventUserAction:
 | 
				
			||||||
 | 
									b.i.Cmd.Action(msg.Channel, username+msg.Text)
 | 
				
			||||||
 | 
								case config.EventNoticeIRC:
 | 
				
			||||||
 | 
									b.Log.Debugf("Sending notice to channel %s", msg.Channel)
 | 
				
			||||||
 | 
									b.i.Cmd.Notice(msg.Channel, username+msg.Text)
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									b.Log.Debugf("Sending to channel %s", msg.Channel)
 | 
				
			||||||
 | 
									b.i.Cmd.Message(msg.Channel, username+msg.Text)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful
 | 
				
			||||||
 | 
					func (b *Birc) getClient() (*girc.Client, error) {
 | 
				
			||||||
 | 
						server, portstr, err := net.SplitHostPort(b.GetString("Server"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						port, err := strconv.Atoi(portstr)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := b.GetString("UserName")
 | 
				
			||||||
 | 
						if user == "" {
 | 
				
			||||||
 | 
							user = b.GetString("Nick")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// fix strict user handling of girc
 | 
				
			||||||
 | 
						for !girc.IsValidUser(user) {
 | 
				
			||||||
 | 
							if len(user) == 1 || len(user) == 0 {
 | 
				
			||||||
 | 
								user = "matterbridge"
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							user = user[1:]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						realName := b.GetString("RealName")
 | 
				
			||||||
 | 
						if realName == "" {
 | 
				
			||||||
 | 
							realName = b.GetString("Nick")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						debug := ioutil.Discard
 | 
				
			||||||
 | 
						if b.GetInt("DebugLevel") == 2 {
 | 
				
			||||||
 | 
							debug = b.Log.Writer()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pingDelay, err := time.ParseDuration(b.GetString("pingdelay"))
 | 
				
			||||||
 | 
						if err != nil || pingDelay == 0 {
 | 
				
			||||||
 | 
							pingDelay = time.Minute
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("setting pingdelay to %s", pingDelay)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tlsConfig, err := b.getTLSConfig()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						i := girc.New(girc.Config{
 | 
				
			||||||
 | 
							Server:     server,
 | 
				
			||||||
 | 
							ServerPass: b.GetString("Password"),
 | 
				
			||||||
 | 
							Port:       port,
 | 
				
			||||||
 | 
							Nick:       b.GetString("Nick"),
 | 
				
			||||||
 | 
							User:       user,
 | 
				
			||||||
 | 
							Name:       realName,
 | 
				
			||||||
 | 
							SSL:        b.GetBool("UseTLS"),
 | 
				
			||||||
 | 
							Bind:       b.GetString("Bind"),
 | 
				
			||||||
 | 
							TLSConfig:  tlsConfig,
 | 
				
			||||||
 | 
							PingDelay:  pingDelay,
 | 
				
			||||||
 | 
							// skip gIRC internal rate limiting, since we have our own throttling
 | 
				
			||||||
 | 
							AllowFlood:    true,
 | 
				
			||||||
 | 
							Debug:         debug,
 | 
				
			||||||
 | 
							SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return i, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) endNames(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						channel := event.Params[1]
 | 
				
			||||||
 | 
						sort.Strings(b.names[channel])
 | 
				
			||||||
 | 
						maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
 | 
				
			||||||
 | 
						for len(b.names[channel]) > maxNamesPerPost {
 | 
				
			||||||
 | 
							b.Remote <- config.Message{
 | 
				
			||||||
 | 
								Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
 | 
				
			||||||
 | 
								Channel: channel, Account: b.Account,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.names[channel] = b.names[channel][maxNamesPerPost:]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Remote <- config.Message{
 | 
				
			||||||
 | 
							Username: b.Nick, Text: b.formatnicks(b.names[channel]),
 | 
				
			||||||
 | 
							Channel: channel, Account: b.Account,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.names[channel] = nil
 | 
				
			||||||
 | 
						b.i.Handlers.Clear(girc.RPL_NAMREPLY)
 | 
				
			||||||
 | 
						b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) skipPrivMsg(event girc.Event) bool {
 | 
				
			||||||
 | 
						// Our nick can be changed
 | 
				
			||||||
 | 
						b.Nick = b.i.GetNick()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// freenode doesn't send 001 as first reply
 | 
				
			||||||
 | 
						if event.Command == "NOTICE" && len(event.Params) != 2 {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// don't forward queries to the bot
 | 
				
			||||||
 | 
						if event.Params[0] == b.Nick {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// don't forward message from ourself
 | 
				
			||||||
 | 
						if event.Source != nil {
 | 
				
			||||||
 | 
							if event.Source.Name == b.Nick {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// don't forward messages we sent via RELAYMSG
 | 
				
			||||||
 | 
						if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// This is the old name of the cap sent in spoofed messages; I've kept this in
 | 
				
			||||||
 | 
						// for compatibility reasons
 | 
				
			||||||
 | 
						if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) nicksPerRow() int {
 | 
				
			||||||
 | 
						return 4
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
 | 
				
			||||||
 | 
						channel := event.Params[2]
 | 
				
			||||||
 | 
						b.names[channel] = append(
 | 
				
			||||||
 | 
							b.names[channel],
 | 
				
			||||||
 | 
							strings.Split(strings.TrimSpace(event.Last()), " ")...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) formatnicks(nicks []string) string {
 | 
				
			||||||
 | 
						return strings.Join(nicks, ", ") + " currently on IRC"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Birc) getTLSConfig() (*tls.Config, error) {
 | 
				
			||||||
 | 
						server, _, _ := net.SplitHostPort(b.GetString("server"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tlsConfig := &tls.Config{
 | 
				
			||||||
 | 
							InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec
 | 
				
			||||||
 | 
							ServerName:         server,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if filename := b.GetString("TLSClientCertificate"); filename != "" {
 | 
				
			||||||
 | 
							cert, err := tls.LoadX509KeyPair(filename, filename)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tlsConfig.Certificates = []tls.Certificate{cert}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return tlsConfig, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										59
									
								
								bridge/keybase/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								bridge/keybase/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					package bkeybase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bkeybase) handleKeybase() {
 | 
				
			||||||
 | 
						sub, err := b.kbc.ListenForNewTextMessages()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Error listening: %s", err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								msg, err := sub.Read()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("failed to read message: %s", err.Error())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if msg.Message.Content.TypeName != "text" {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if msg.Message.Sender.Username == b.kbc.GetUsername() {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								b.handleMessage(msg.Message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bkeybase) handleMessage(msg chat1.MsgSummary) {
 | 
				
			||||||
 | 
						b.Log.Debugf("== Receiving event: %#v", msg)
 | 
				
			||||||
 | 
						if msg.Channel.TopicName != b.channel || msg.Channel.Name != b.team {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Sender.Username != b.kbc.GetUsername() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO download avatar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create our message
 | 
				
			||||||
 | 
							rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: string(msg.Sender.Uid), Channel: msg.Channel.TopicName, ID: strconv.Itoa(int(msg.Id)), Account: b.Account}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Text must be a string
 | 
				
			||||||
 | 
							if msg.Content.TypeName != "text" {
 | 
				
			||||||
 | 
								b.Log.Errorf("message is not text")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Sending message from %s on %s to gateway", msg.Sender.Username, msg.Channel.Name)
 | 
				
			||||||
 | 
							b.Remote <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										106
									
								
								bridge/keybase/keybase.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								bridge/keybase/keybase.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					package bkeybase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/keybase/go-keybase-chat-bot/kbchat"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Bkeybase bridge structure
 | 
				
			||||||
 | 
					type Bkeybase struct {
 | 
				
			||||||
 | 
						kbc     *kbchat.API
 | 
				
			||||||
 | 
						user    string
 | 
				
			||||||
 | 
						channel string
 | 
				
			||||||
 | 
						team    string
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New initializes Bkeybase object and sets team
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &Bkeybase{Config: cfg}
 | 
				
			||||||
 | 
						b.team = b.Config.GetString("Team")
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Connect starts keybase API and listener loop
 | 
				
			||||||
 | 
					func (b *Bkeybase) Connect() error {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s", b.GetString("Team"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// use default keybase location (`keybase`)
 | 
				
			||||||
 | 
						b.kbc, err = kbchat.Start(kbchat.RunOptions{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.user = b.kbc.GetUsername()
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						go b.handleKeybase()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Disconnect doesn't do anything for now
 | 
				
			||||||
 | 
					func (b *Bkeybase) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// JoinChannel sets channel name in struct
 | 
				
			||||||
 | 
					func (b *Bkeybase) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						if _, err := b.kbc.JoinChannel(b.team, channel.Name); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.channel = channel.Name
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Send receives bridge messages and sends them to Keybase chat room
 | 
				
			||||||
 | 
					func (b *Bkeybase) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle /me events
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
							msg.Text = "_" + msg.Text + "_"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message if we have an ID
 | 
				
			||||||
 | 
						// Delete message not supported by keybase go library yet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message if we have an ID
 | 
				
			||||||
 | 
						// kbchat lib does not support message editing yet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
							// Upload a file
 | 
				
			||||||
 | 
							dir, err := ioutil.TempDir("", "matterbridge")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer os.RemoveAll(dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
								fname := f.(config.FileInfo).Name
 | 
				
			||||||
 | 
								fdata := *f.(config.FileInfo).Data
 | 
				
			||||||
 | 
								fcaption := f.(config.FileInfo).Comment
 | 
				
			||||||
 | 
								fpath := filepath.Join(dir, fname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err = ioutil.WriteFile(fpath, fdata, 0600); err != nil {
 | 
				
			||||||
 | 
									return "", err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, _ = b.kbc.SendAttachmentByTeam(b.team, &b.channel, fpath, fcaption)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Send regular message
 | 
				
			||||||
 | 
						text := msg.Username + msg.Text
 | 
				
			||||||
 | 
						resp, err := b.kbc.SendMessageByTeamName(b.team, &b.channel, text)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return strconv.Itoa(int(*resp.Result.MessageID)), err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										215
									
								
								bridge/matrix/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								bridge/matrix/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,215 @@
 | 
				
			|||||||
 | 
					package bmatrix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						matrix "github.com/matterbridge/gomatrix"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newMatrixUsername(username string) *matrixUsername {
 | 
				
			||||||
 | 
						mUsername := new(matrixUsername)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check if we have a </tag>. if we have, we don't escape HTML. #696
 | 
				
			||||||
 | 
						if htmlTag.MatchString(username) {
 | 
				
			||||||
 | 
							mUsername.formatted = username
 | 
				
			||||||
 | 
							// remove the HTML formatting for beautiful push messages #1188
 | 
				
			||||||
 | 
							mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							mUsername.formatted = html.EscapeString(username)
 | 
				
			||||||
 | 
							mUsername.plain = username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return mUsername
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getRoomID retrieves a matching room ID from the channel name.
 | 
				
			||||||
 | 
					func (b *Bmatrix) getRoomID(channel string) string {
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						defer b.RUnlock()
 | 
				
			||||||
 | 
						for ID, name := range b.RoomMap {
 | 
				
			||||||
 | 
							if name == channel {
 | 
				
			||||||
 | 
								return ID
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// interface2Struct marshals and immediately unmarshals an interface.
 | 
				
			||||||
 | 
					// Useful for converting map[string]interface{} to a struct.
 | 
				
			||||||
 | 
					func interface2Struct(in interface{}, out interface{}) error {
 | 
				
			||||||
 | 
						jsonObj, err := json.Marshal(in)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err //nolint:wrapcheck
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return json.Unmarshal(jsonObj, out)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache.
 | 
				
			||||||
 | 
					func (b *Bmatrix) getDisplayName(mxid string) string {
 | 
				
			||||||
 | 
						if b.GetBool("UseUserName") {
 | 
				
			||||||
 | 
							return mxid[1:]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						if val, present := b.NicknameMap[mxid]; present {
 | 
				
			||||||
 | 
							b.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return val.displayName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						displayName, err := b.mc.GetDisplayName(mxid)
 | 
				
			||||||
 | 
						var httpError *matrix.HTTPError
 | 
				
			||||||
 | 
						if errors.As(err, &httpError) {
 | 
				
			||||||
 | 
							b.Log.Warnf("Couldn't retrieve the display name for %s", mxid)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return b.cacheDisplayName(mxid, mxid[1:])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.cacheDisplayName(mxid, displayName.DisplayName)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver.
 | 
				
			||||||
 | 
					// Note that old entries are cleaned when this function is called.
 | 
				
			||||||
 | 
					func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
 | 
				
			||||||
 | 
						now := time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// scan to delete old entries, to stop memory usage from becoming too high with old entries.
 | 
				
			||||||
 | 
						// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them.
 | 
				
			||||||
 | 
						toDelete := []string{}
 | 
				
			||||||
 | 
						conflict := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Lock()
 | 
				
			||||||
 | 
						for mxid, v := range b.NicknameMap {
 | 
				
			||||||
 | 
							// to prevent username reuse across matrix servers - or even on the same server, append
 | 
				
			||||||
 | 
							// the mxid to the username when there is a conflict
 | 
				
			||||||
 | 
							if v.displayName == displayName {
 | 
				
			||||||
 | 
								conflict = true
 | 
				
			||||||
 | 
								// TODO: it would be nice to be able to rename previous messages from this user.
 | 
				
			||||||
 | 
								// The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior.
 | 
				
			||||||
 | 
								v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
 | 
				
			||||||
 | 
								b.NicknameMap[mxid] = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if now.Sub(v.lastUpdated) > 10*time.Minute {
 | 
				
			||||||
 | 
								toDelete = append(toDelete, mxid)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if conflict {
 | 
				
			||||||
 | 
							displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, v := range toDelete {
 | 
				
			||||||
 | 
							delete(b.NicknameMap, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.NicknameMap[mxid] = NicknameCacheEntry{
 | 
				
			||||||
 | 
							displayName: displayName,
 | 
				
			||||||
 | 
							lastUpdated: now,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return displayName
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleError converts errors into httpError.
 | 
				
			||||||
 | 
					//nolint:exhaustivestruct
 | 
				
			||||||
 | 
					func handleError(err error) *httpError {
 | 
				
			||||||
 | 
						var mErr matrix.HTTPError
 | 
				
			||||||
 | 
						if !errors.As(err, &mErr) {
 | 
				
			||||||
 | 
							return &httpError{
 | 
				
			||||||
 | 
								Err: "not a HTTPError",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var httpErr httpError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil {
 | 
				
			||||||
 | 
							return &httpError{
 | 
				
			||||||
 | 
								Err: "unmarshal failed",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &httpErr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
 | 
				
			||||||
 | 
						// Skip empty messages
 | 
				
			||||||
 | 
						if content["msgtype"] == nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Only allow image,video or file msgtypes
 | 
				
			||||||
 | 
						if !(content["msgtype"].(string) == "m.image" ||
 | 
				
			||||||
 | 
							content["msgtype"].(string) == "m.video" ||
 | 
				
			||||||
 | 
							content["msgtype"].(string) == "m.file") {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getAvatarURL returns the avatar URL of the specified sender.
 | 
				
			||||||
 | 
					func (b *Bmatrix) getAvatarURL(sender string) string {
 | 
				
			||||||
 | 
						urlPath := b.mc.BuildURL("profile", sender, "avatar_url")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s := struct {
 | 
				
			||||||
 | 
							AvatarURL string `json:"avatar_url"`
 | 
				
			||||||
 | 
						}{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := b.mc.MakeRequest("GET", urlPath, nil, &s)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("getAvatarURL failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/")
 | 
				
			||||||
 | 
						if url != "" {
 | 
				
			||||||
 | 
							url += "?width=37&height=37&method=crop"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return url
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) {
 | 
				
			||||||
 | 
						httpErr := handleError(err)
 | 
				
			||||||
 | 
						if httpErr.Errcode != "M_LIMIT_EXCEEDED" {
 | 
				
			||||||
 | 
							return 0, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("ratelimited: %s", httpErr.Err)
 | 
				
			||||||
 | 
						b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// retry function will check if we're ratelimited and retries again when backoff time expired
 | 
				
			||||||
 | 
					// returns original error if not 429 ratelimit
 | 
				
			||||||
 | 
					func (b *Bmatrix) retry(f func() error) error {
 | 
				
			||||||
 | 
						b.rateMutex.Lock()
 | 
				
			||||||
 | 
						defer b.rateMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							if err := f(); err != nil {
 | 
				
			||||||
 | 
								if backoff, ok := b.handleRatelimit(err); ok {
 | 
				
			||||||
 | 
									time.Sleep(backoff)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										694
									
								
								bridge/matrix/matrix.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										694
									
								
								bridge/matrix/matrix.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,694 @@
 | 
				
			|||||||
 | 
					package bmatrix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						matrix "github.com/matterbridge/gomatrix"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						htmlTag            = regexp.MustCompile("</.*?>")
 | 
				
			||||||
 | 
						htmlReplacementTag = regexp.MustCompile("<[^>]*>")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type NicknameCacheEntry struct {
 | 
				
			||||||
 | 
						displayName string
 | 
				
			||||||
 | 
						lastUpdated time.Time
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bmatrix struct {
 | 
				
			||||||
 | 
						mc          *matrix.Client
 | 
				
			||||||
 | 
						UserID      string
 | 
				
			||||||
 | 
						NicknameMap map[string]NicknameCacheEntry
 | 
				
			||||||
 | 
						RoomMap     map[string]string
 | 
				
			||||||
 | 
						rateMutex   sync.RWMutex
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type httpError struct {
 | 
				
			||||||
 | 
						Errcode      string `json:"errcode"`
 | 
				
			||||||
 | 
						Err          string `json:"error"`
 | 
				
			||||||
 | 
						RetryAfterMs int    `json:"retry_after_ms"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type matrixUsername struct {
 | 
				
			||||||
 | 
						plain     string
 | 
				
			||||||
 | 
						formatted string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubTextMessage represents the new content of the message in edit messages.
 | 
				
			||||||
 | 
					type SubTextMessage struct {
 | 
				
			||||||
 | 
						MsgType       string `json:"msgtype"`
 | 
				
			||||||
 | 
						Body          string `json:"body"`
 | 
				
			||||||
 | 
						FormattedBody string `json:"formatted_body,omitempty"`
 | 
				
			||||||
 | 
						Format        string `json:"format,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MessageRelation explains how the current message relates to a previous message.
 | 
				
			||||||
 | 
					// Notably used for message edits.
 | 
				
			||||||
 | 
					type MessageRelation struct {
 | 
				
			||||||
 | 
						EventID string `json:"event_id"`
 | 
				
			||||||
 | 
						Type    string `json:"rel_type"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type EditedMessage struct {
 | 
				
			||||||
 | 
						NewContent SubTextMessage  `json:"m.new_content"`
 | 
				
			||||||
 | 
						RelatedTo  MessageRelation `json:"m.relates_to"`
 | 
				
			||||||
 | 
						matrix.TextMessage
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type InReplyToRelationContent struct {
 | 
				
			||||||
 | 
						EventID string `json:"event_id"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type InReplyToRelation struct {
 | 
				
			||||||
 | 
						InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ReplyMessage struct {
 | 
				
			||||||
 | 
						RelatedTo InReplyToRelation `json:"m.relates_to"`
 | 
				
			||||||
 | 
						matrix.TextMessage
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &Bmatrix{Config: cfg}
 | 
				
			||||||
 | 
						b.RoomMap = make(map[string]string)
 | 
				
			||||||
 | 
						b.NicknameMap = make(map[string]NicknameCacheEntry)
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) Connect() error {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
				
			||||||
 | 
						if b.GetString("MxID") != "" && b.GetString("Token") != "" {
 | 
				
			||||||
 | 
							b.mc, err = matrix.NewClient(
 | 
				
			||||||
 | 
								b.GetString("Server"), b.GetString("MxID"), b.GetString("Token"),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.UserID = b.GetString("MxID")
 | 
				
			||||||
 | 
							b.Log.Info("Using existing Matrix credentials")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							resp, err := b.mc.Login(&matrix.ReqLogin{
 | 
				
			||||||
 | 
								Type:       "m.login.password",
 | 
				
			||||||
 | 
								User:       b.GetString("Login"),
 | 
				
			||||||
 | 
								Password:   b.GetString("Password"),
 | 
				
			||||||
 | 
								Identifier: matrix.NewUserIdentifier(b.GetString("Login")),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.mc.SetCredentials(resp.UserID, resp.AccessToken)
 | 
				
			||||||
 | 
							b.UserID = resp.UserID
 | 
				
			||||||
 | 
							b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						go b.handlematrix()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						return b.retry(func() error {
 | 
				
			||||||
 | 
							resp, err := b.mc.JoinRoom(channel.Name, "", nil)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Lock()
 | 
				
			||||||
 | 
							b.RoomMap[resp.RoomID] = channel.Name
 | 
				
			||||||
 | 
							b.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channel := b.getRoomID(msg.Channel)
 | 
				
			||||||
 | 
						b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						username := newMatrixUsername(msg.Username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make a action /me of the message
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
							m := matrix.TextMessage{
 | 
				
			||||||
 | 
								MsgType:       "m.emote",
 | 
				
			||||||
 | 
								Body:          username.plain + msg.Text,
 | 
				
			||||||
 | 
								FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
 | 
				
			||||||
 | 
								Format:        "org.matrix.custom.html",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.GetBool("HTMLDisable") {
 | 
				
			||||||
 | 
								m.Format = ""
 | 
				
			||||||
 | 
								m.FormattedBody = ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							msgID := ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := b.retry(func() error {
 | 
				
			||||||
 | 
								resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								msgID = resp.EventID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return msgID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							msgID := ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := b.retry(func() error {
 | 
				
			||||||
 | 
								resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								msgID = resp.EventID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return msgID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file if it exists
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								rmsg := rmsg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err := b.retry(func() error {
 | 
				
			||||||
 | 
									_, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("sendText failed: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// check if we have files to upload (from slack, telegram or mattermost)
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return b.handleUploadFiles(&msg, channel)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message if we have an ID
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							rmsg := EditedMessage{
 | 
				
			||||||
 | 
								TextMessage: matrix.TextMessage{
 | 
				
			||||||
 | 
									Body:          username.plain + msg.Text,
 | 
				
			||||||
 | 
									MsgType:       "m.text",
 | 
				
			||||||
 | 
									Format:        "org.matrix.custom.html",
 | 
				
			||||||
 | 
									FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rmsg.NewContent = SubTextMessage{
 | 
				
			||||||
 | 
								Body:          rmsg.TextMessage.Body,
 | 
				
			||||||
 | 
								FormattedBody: rmsg.TextMessage.FormattedBody,
 | 
				
			||||||
 | 
								Format:        rmsg.TextMessage.Format,
 | 
				
			||||||
 | 
								MsgType:       "m.text",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.GetBool("HTMLDisable") {
 | 
				
			||||||
 | 
								rmsg.TextMessage.Format = ""
 | 
				
			||||||
 | 
								rmsg.TextMessage.FormattedBody = ""
 | 
				
			||||||
 | 
								rmsg.NewContent.Format = ""
 | 
				
			||||||
 | 
								rmsg.NewContent.FormattedBody = ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rmsg.RelatedTo = MessageRelation{
 | 
				
			||||||
 | 
								EventID: msg.ID,
 | 
				
			||||||
 | 
								Type:    "m.replace",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := b.retry(func() error {
 | 
				
			||||||
 | 
								_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return msg.ID, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use notices to send join/leave events
 | 
				
			||||||
 | 
						if msg.Event == config.EventJoinLeave {
 | 
				
			||||||
 | 
							m := matrix.TextMessage{
 | 
				
			||||||
 | 
								MsgType:       "m.notice",
 | 
				
			||||||
 | 
								Body:          username.plain + msg.Text,
 | 
				
			||||||
 | 
								FormattedBody: username.formatted + msg.Text,
 | 
				
			||||||
 | 
								Format:        "org.matrix.custom.html",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.GetBool("HTMLDisable") {
 | 
				
			||||||
 | 
								m.Format = ""
 | 
				
			||||||
 | 
								m.FormattedBody = ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								resp *matrix.RespSendEvent
 | 
				
			||||||
 | 
								err  error
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = b.retry(func() error {
 | 
				
			||||||
 | 
								resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return resp.EventID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.ParentValid() {
 | 
				
			||||||
 | 
							m := ReplyMessage{
 | 
				
			||||||
 | 
								TextMessage: matrix.TextMessage{
 | 
				
			||||||
 | 
									MsgType:       "m.text",
 | 
				
			||||||
 | 
									Body:          username.plain + msg.Text,
 | 
				
			||||||
 | 
									FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
 | 
				
			||||||
 | 
									Format:        "org.matrix.custom.html",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.GetBool("HTMLDisable") {
 | 
				
			||||||
 | 
								m.TextMessage.Format = ""
 | 
				
			||||||
 | 
								m.TextMessage.FormattedBody = ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							m.RelatedTo = InReplyToRelation{
 | 
				
			||||||
 | 
								InReplyTo: InReplyToRelationContent{
 | 
				
			||||||
 | 
									EventID: msg.ParentID,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								resp *matrix.RespSendEvent
 | 
				
			||||||
 | 
								err  error
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = b.retry(func() error {
 | 
				
			||||||
 | 
								resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return resp.EventID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("HTMLDisable") {
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								resp *matrix.RespSendEvent
 | 
				
			||||||
 | 
								err  error
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = b.retry(func() error {
 | 
				
			||||||
 | 
								resp, err = b.mc.SendText(channel, username.plain+msg.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return resp.EventID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post normal message with HTML support (eg riot.im)
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							resp *matrix.RespSendEvent
 | 
				
			||||||
 | 
							err  error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = b.retry(func() error {
 | 
				
			||||||
 | 
							resp, err = b.mc.SendFormattedText(channel, username.plain+msg.Text,
 | 
				
			||||||
 | 
								username.formatted+helper.ParseMarkdown(msg.Text))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return resp.EventID, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) handlematrix() {
 | 
				
			||||||
 | 
						syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
 | 
				
			||||||
 | 
						syncer.OnEventType("m.room.redaction", b.handleEvent)
 | 
				
			||||||
 | 
						syncer.OnEventType("m.room.message", b.handleEvent)
 | 
				
			||||||
 | 
						syncer.OnEventType("m.room.member", b.handleMemberChange)
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								if b == nil {
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := b.mc.Sync(); err != nil {
 | 
				
			||||||
 | 
									b.Log.Println("Sync() returned ", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
 | 
				
			||||||
 | 
						relationInterface, present := ev.Content["m.relates_to"]
 | 
				
			||||||
 | 
						newContentInterface, present2 := ev.Content["m.new_content"]
 | 
				
			||||||
 | 
						if !(present && present2) {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var relation MessageRelation
 | 
				
			||||||
 | 
						if err := interface2Struct(relationInterface, &relation); err != nil {
 | 
				
			||||||
 | 
							b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface)
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var newContent SubTextMessage
 | 
				
			||||||
 | 
						if err := interface2Struct(newContentInterface, &newContent); err != nil {
 | 
				
			||||||
 | 
							b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface)
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if relation.Type != "m.replace" {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg.ID = relation.EventID
 | 
				
			||||||
 | 
						rmsg.Text = newContent.Body
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool {
 | 
				
			||||||
 | 
						relationInterface, present := ev.Content["m.relates_to"]
 | 
				
			||||||
 | 
						if !present {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var relation InReplyToRelation
 | 
				
			||||||
 | 
						if err := interface2Struct(relationInterface, &relation); err != nil {
 | 
				
			||||||
 | 
							// probably fine
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						body := rmsg.Text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !b.GetBool("keepquotedreply") {
 | 
				
			||||||
 | 
							for strings.HasPrefix(body, "> ") {
 | 
				
			||||||
 | 
								lineIdx := strings.IndexRune(body, '\n')
 | 
				
			||||||
 | 
								if lineIdx == -1 {
 | 
				
			||||||
 | 
									body = ""
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									body = body[(lineIdx + 1):]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg.Text = body
 | 
				
			||||||
 | 
						rmsg.ParentID = relation.InReplyTo.EventID
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleMemberChange(ev *matrix.Event) {
 | 
				
			||||||
 | 
						// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
 | 
				
			||||||
 | 
						if ev.Content["membership"] == "join" {
 | 
				
			||||||
 | 
							if dn, ok := ev.Content["displayname"].(string); ok {
 | 
				
			||||||
 | 
								b.cacheDisplayName(ev.Sender, dn)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 | 
				
			||||||
 | 
						b.Log.Debugf("== Receiving event: %#v", ev)
 | 
				
			||||||
 | 
						if ev.Sender != b.UserID {
 | 
				
			||||||
 | 
							b.RLock()
 | 
				
			||||||
 | 
							channel, ok := b.RoomMap[ev.RoomID]
 | 
				
			||||||
 | 
							b.RUnlock()
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								b.Log.Debugf("Unknown room %s", ev.RoomID)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create our message
 | 
				
			||||||
 | 
							rmsg := config.Message{
 | 
				
			||||||
 | 
								Username: b.getDisplayName(ev.Sender),
 | 
				
			||||||
 | 
								Channel:  channel,
 | 
				
			||||||
 | 
								Account:  b.Account,
 | 
				
			||||||
 | 
								UserID:   ev.Sender,
 | 
				
			||||||
 | 
								ID:       ev.ID,
 | 
				
			||||||
 | 
								Avatar:   b.getAvatarURL(ev.Sender),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Remove homeserver suffix if configured
 | 
				
			||||||
 | 
							if b.GetBool("NoHomeServerSuffix") {
 | 
				
			||||||
 | 
								re := regexp.MustCompile("(.*?):.*")
 | 
				
			||||||
 | 
								rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Delete event
 | 
				
			||||||
 | 
							if ev.Type == "m.room.redaction" {
 | 
				
			||||||
 | 
								rmsg.Event = config.EventMsgDelete
 | 
				
			||||||
 | 
								rmsg.ID = ev.Redacts
 | 
				
			||||||
 | 
								rmsg.Text = config.EventMsgDelete
 | 
				
			||||||
 | 
								b.Remote <- rmsg
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Text must be a string
 | 
				
			||||||
 | 
							if rmsg.Text, ok = ev.Content["body"].(string); !ok {
 | 
				
			||||||
 | 
								b.Log.Errorf("Content[body] is not a string: %T\n%#v",
 | 
				
			||||||
 | 
									ev.Content["body"], ev.Content)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Do we have a /me action
 | 
				
			||||||
 | 
							if ev.Content["msgtype"].(string) == "m.emote" {
 | 
				
			||||||
 | 
								rmsg.Event = config.EventUserAction
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Is it an edit?
 | 
				
			||||||
 | 
							if b.handleEdit(ev, rmsg) {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Is it a reply?
 | 
				
			||||||
 | 
							if b.handleReply(ev, rmsg) {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Do we have attachments
 | 
				
			||||||
 | 
							if b.containsAttachment(ev.Content) {
 | 
				
			||||||
 | 
								err := b.handleDownloadFile(&rmsg, ev.Content)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("download failed: %#v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
 | 
				
			||||||
 | 
							b.Remote <- rmsg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// not crucial, so no ratelimit check here
 | 
				
			||||||
 | 
							if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("couldn't mark message as read %s", err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadFile handles file download
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]interface{}) error {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							ok                        bool
 | 
				
			||||||
 | 
							url, name, msgtype, mtype string
 | 
				
			||||||
 | 
							info                      map[string]interface{}
 | 
				
			||||||
 | 
							size                      float64
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg.Extra = make(map[string][]interface{})
 | 
				
			||||||
 | 
						if url, ok = content["url"].(string); !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("url isn't a %T", url)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						url = strings.Replace(url, "mxc://", b.GetString("Server")+"/_matrix/media/v1/download/", -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if info, ok = content["info"].(map[string]interface{}); !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("info isn't a %T", info)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if size, ok = info["size"].(float64); !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("size isn't a %T", size)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if name, ok = content["body"].(string); !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("name isn't a %T", name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if msgtype, ok = content["msgtype"].(string); !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("msgtype isn't a %T", msgtype)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if mtype, ok = info["mimetype"].(string); !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("mtype isn't a %T", mtype)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check if we have an image uploaded without extension
 | 
				
			||||||
 | 
						if !strings.Contains(name, ".") {
 | 
				
			||||||
 | 
							if msgtype == "m.image" {
 | 
				
			||||||
 | 
								mext, _ := mime.ExtensionsByType(mtype)
 | 
				
			||||||
 | 
								if len(mext) > 0 {
 | 
				
			||||||
 | 
									name += mext[0]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// just a default .png extension if we don't have mime info
 | 
				
			||||||
 | 
								name += ".png"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check if the size is ok
 | 
				
			||||||
 | 
						err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// actually download the file
 | 
				
			||||||
 | 
						data, err := helper.DownloadFile(url)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("download %s failed %#v", url, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// add the downloaded data to the message
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleUploadFiles handles native upload of files.
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string, error) {
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							if fi, ok := f.(config.FileInfo); ok {
 | 
				
			||||||
 | 
								b.handleUploadFile(msg, channel, &fi)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleUploadFile handles native upload of a file.
 | 
				
			||||||
 | 
					func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) {
 | 
				
			||||||
 | 
						username := newMatrixUsername(msg.Username)
 | 
				
			||||||
 | 
						content := bytes.NewReader(*fi.Data)
 | 
				
			||||||
 | 
						sp := strings.Split(fi.Name, ".")
 | 
				
			||||||
 | 
						mtype := mime.TypeByExtension("." + sp[len(sp)-1])
 | 
				
			||||||
 | 
						// image and video uploads send no username, we have to do this ourself here #715
 | 
				
			||||||
 | 
						err := b.retry(func() error {
 | 
				
			||||||
 | 
							_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("file comment failed: %#v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var res *matrix.RespMediaUpload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = b.retry(func() error {
 | 
				
			||||||
 | 
							res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("file upload failed: %#v", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case strings.Contains(mtype, "video"):
 | 
				
			||||||
 | 
							b.Log.Debugf("sendVideo %s", res.ContentURI)
 | 
				
			||||||
 | 
							err = b.retry(func() error {
 | 
				
			||||||
 | 
								_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("sendVideo failed: %#v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case strings.Contains(mtype, "image"):
 | 
				
			||||||
 | 
							b.Log.Debugf("sendImage %s", res.ContentURI)
 | 
				
			||||||
 | 
							err = b.retry(func() error {
 | 
				
			||||||
 | 
								_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("sendImage failed: %#v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case strings.Contains(mtype, "audio"):
 | 
				
			||||||
 | 
							b.Log.Debugf("sendAudio %s", res.ContentURI)
 | 
				
			||||||
 | 
							err = b.retry(func() error {
 | 
				
			||||||
 | 
								_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
 | 
				
			||||||
 | 
									MsgType: "m.audio",
 | 
				
			||||||
 | 
									Body:    fi.Name,
 | 
				
			||||||
 | 
									URL:     res.ContentURI,
 | 
				
			||||||
 | 
									Info: matrix.AudioInfo{
 | 
				
			||||||
 | 
										Mimetype: mtype,
 | 
				
			||||||
 | 
										Size:     uint(len(*fi.Data)),
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("sendAudio failed: %#v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							b.Log.Debugf("sendFile %s", res.ContentURI)
 | 
				
			||||||
 | 
							err = b.retry(func() error {
 | 
				
			||||||
 | 
								_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
 | 
				
			||||||
 | 
									MsgType: "m.file",
 | 
				
			||||||
 | 
									Body:    fi.Name,
 | 
				
			||||||
 | 
									URL:     res.ContentURI,
 | 
				
			||||||
 | 
									Info: matrix.FileInfo{
 | 
				
			||||||
 | 
										Mimetype: mtype,
 | 
				
			||||||
 | 
										Size:     uint(len(*fi.Data)),
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("sendFile failed: %#v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("result: %#v", res)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								bridge/matrix/matrix_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								bridge/matrix/matrix_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					package bmatrix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPlainUsername(t *testing.T) {
 | 
				
			||||||
 | 
						uut := newMatrixUsername("MyUser")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, "MyUser", uut.formatted)
 | 
				
			||||||
 | 
						assert.Equal(t, "MyUser", uut.plain)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHTMLUsername(t *testing.T) {
 | 
				
			||||||
 | 
						uut := newMatrixUsername("<b>MyUser</b>")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, "<b>MyUser</b>", uut.formatted)
 | 
				
			||||||
 | 
						assert.Equal(t, "MyUser", uut.plain)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestFancyUsername(t *testing.T) {
 | 
				
			||||||
 | 
						uut := newMatrixUsername("<MyUser>")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, "<MyUser>", uut.formatted)
 | 
				
			||||||
 | 
						assert.Equal(t, "<MyUser>", uut.plain)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										366
									
								
								bridge/mattermost/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								bridge/mattermost/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,366 @@
 | 
				
			|||||||
 | 
					package bmattermost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterclient"
 | 
				
			||||||
 | 
						matterclient6 "github.com/matterbridge/matterclient"
 | 
				
			||||||
 | 
						"github.com/mattermost/mattermost-server/v5/model"
 | 
				
			||||||
 | 
						model6 "github.com/mattermost/mattermost-server/v6/model"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadAvatar downloads the avatar of userid from channel
 | 
				
			||||||
 | 
					// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
 | 
				
			||||||
 | 
					// logs an error message if it fails
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							Username: "system",
 | 
				
			||||||
 | 
							Text:     "avatar",
 | 
				
			||||||
 | 
							Channel:  channel,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							UserID:   userid,
 | 
				
			||||||
 | 
							Event:    config.EventAvatarDownload,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, ok := b.avatarMap[userid]; !ok {
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								data []byte
 | 
				
			||||||
 | 
								err  error
 | 
				
			||||||
 | 
								resp *model.Response
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if b.mc6 != nil {
 | 
				
			||||||
 | 
								data, _, err = b.mc6.Client.GetProfileImage(userid, "")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								data, resp = b.mc.Client.GetProfileImage(userid, "")
 | 
				
			||||||
 | 
								if resp.Error != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Error(err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General)
 | 
				
			||||||
 | 
							b.Remote <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadFile handles file download
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
 | 
				
			||||||
 | 
						if b.mc6 != nil {
 | 
				
			||||||
 | 
							return b.handleDownloadFile6(rmsg, id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						url, _ := b.mc.Client.GetFileLink(id)
 | 
				
			||||||
 | 
						finfo, resp := b.mc.Client.GetFileInfo(id)
 | 
				
			||||||
 | 
						if resp.Error != nil {
 | 
				
			||||||
 | 
							return resp.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						data, resp := b.mc.Client.DownloadFile(id, true)
 | 
				
			||||||
 | 
						if resp.Error != nil {
 | 
				
			||||||
 | 
							return resp.Error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint:wrapcheck
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleDownloadFile6(rmsg *config.Message, id string) error {
 | 
				
			||||||
 | 
						url, _, _ := b.mc6.Client.GetFileLink(id)
 | 
				
			||||||
 | 
						finfo, _, err := b.mc6.Client.GetFileInfo(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						data, _, err := b.mc6.Client.DownloadFile(id, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleMatter() {
 | 
				
			||||||
 | 
						messages := make(chan *config.Message)
 | 
				
			||||||
 | 
						if b.GetString("WebhookBindAddress") != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Choosing webhooks based receiving")
 | 
				
			||||||
 | 
							go b.handleMatterHook(messages)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if b.GetString("Token") != "" {
 | 
				
			||||||
 | 
								b.Log.Debugf("Choosing token based receiving")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								b.Log.Debugf("Choosing login/password based receiving")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// if for some reason we only want to sent stuff to mattermost but not receive, return
 | 
				
			||||||
 | 
							if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") != "" && b.GetString("Token") == "" && b.GetString("Login") == "" {
 | 
				
			||||||
 | 
								b.Log.Debugf("No WebhookBindAddress specified, only WebhookURL. You will not receive messages from mattermost, only sending is possible.")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleMatterClient(messages)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var ok bool
 | 
				
			||||||
 | 
						for message := range messages {
 | 
				
			||||||
 | 
							message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General)
 | 
				
			||||||
 | 
							message.Account = b.Account
 | 
				
			||||||
 | 
							message.Text, ok = b.replaceAction(message.Text)
 | 
				
			||||||
 | 
							if ok {
 | 
				
			||||||
 | 
								message.Event = config.EventUserAction
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Message is %#v", message)
 | 
				
			||||||
 | 
							b.Remote <- *message
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 | 
				
			||||||
 | 
						if b.mc6 != nil {
 | 
				
			||||||
 | 
							b.Log.Debug("starting matterclient6")
 | 
				
			||||||
 | 
							b.handleMatterClient6(messages)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for message := range b.mc.MessageChan {
 | 
				
			||||||
 | 
							b.Log.Debugf("%#v", message.Raw.Data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.skipMessage(message) {
 | 
				
			||||||
 | 
								b.Log.Debugf("Skipped message: %#v", message)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							channelName := b.getChannelName(message.Post.ChannelId)
 | 
				
			||||||
 | 
							if channelName == "" {
 | 
				
			||||||
 | 
								channelName = message.Channel
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// only download avatars if we have a place to upload them (configured mediaserver)
 | 
				
			||||||
 | 
							if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
 | 
				
			||||||
 | 
								b.handleDownloadAvatar(message.UserID, channelName)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("== Receiving event %#v", message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rmsg := &config.Message{
 | 
				
			||||||
 | 
								Username: message.Username,
 | 
				
			||||||
 | 
								UserID:   message.UserID,
 | 
				
			||||||
 | 
								Channel:  channelName,
 | 
				
			||||||
 | 
								Text:     message.Text,
 | 
				
			||||||
 | 
								ID:       message.Post.Id,
 | 
				
			||||||
 | 
								ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
 | 
				
			||||||
 | 
								Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle mattermost post properties (override username and attachments)
 | 
				
			||||||
 | 
							b.handleProps(rmsg, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// create a text for bridges that don't support native editing
 | 
				
			||||||
 | 
							if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
 | 
				
			||||||
 | 
								rmsg.Text = message.Text + b.GetString("EditSuffix")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
 | 
				
			||||||
 | 
								rmsg.Event = config.EventMsgDelete
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, id := range message.Post.FileIds {
 | 
				
			||||||
 | 
								err := b.handleDownloadFile(rmsg, id)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("download failed: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Use nickname instead of username if defined
 | 
				
			||||||
 | 
							if !b.GetBool("useusername") {
 | 
				
			||||||
 | 
								if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
 | 
				
			||||||
 | 
									rmsg.Username = nick
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							messages <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint:cyclop
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) {
 | 
				
			||||||
 | 
						for message := range b.mc6.MessageChan {
 | 
				
			||||||
 | 
							b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.skipMessage6(message) {
 | 
				
			||||||
 | 
								b.Log.Debugf("Skipped message: %#v", message)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							channelName := b.getChannelName(message.Post.ChannelId)
 | 
				
			||||||
 | 
							if channelName == "" {
 | 
				
			||||||
 | 
								channelName = message.Channel
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// only download avatars if we have a place to upload them (configured mediaserver)
 | 
				
			||||||
 | 
							if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
 | 
				
			||||||
 | 
								b.handleDownloadAvatar(message.UserID, channelName)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("== Receiving event %#v", message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rmsg := &config.Message{
 | 
				
			||||||
 | 
								Username: message.Username,
 | 
				
			||||||
 | 
								UserID:   message.UserID,
 | 
				
			||||||
 | 
								Channel:  channelName,
 | 
				
			||||||
 | 
								Text:     message.Text,
 | 
				
			||||||
 | 
								ID:       message.Post.Id,
 | 
				
			||||||
 | 
								ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
 | 
				
			||||||
 | 
								Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle mattermost post properties (override username and attachments)
 | 
				
			||||||
 | 
							b.handleProps6(rmsg, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// create a text for bridges that don't support native editing
 | 
				
			||||||
 | 
							if message.Raw.EventType() == model6.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
 | 
				
			||||||
 | 
								rmsg.Text = message.Text + b.GetString("EditSuffix")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if message.Raw.EventType() == model6.WebsocketEventPostDeleted {
 | 
				
			||||||
 | 
								rmsg.Event = config.EventMsgDelete
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, id := range message.Post.FileIds {
 | 
				
			||||||
 | 
								err := b.handleDownloadFile(rmsg, id)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("download failed: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Use nickname instead of username if defined
 | 
				
			||||||
 | 
							if !b.GetBool("useusername") {
 | 
				
			||||||
 | 
								if nick := b.mc6.GetNickName(rmsg.UserID); nick != "" {
 | 
				
			||||||
 | 
									rmsg.Username = nick
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							messages <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							message := b.mh.Receive()
 | 
				
			||||||
 | 
							b.Log.Debugf("Receiving from matterhook %#v", message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							messages <- &config.Message{
 | 
				
			||||||
 | 
								UserID:   message.UserID,
 | 
				
			||||||
 | 
								Username: message.UserName,
 | 
				
			||||||
 | 
								Text:     message.Text,
 | 
				
			||||||
 | 
								Channel:  message.ChannelName,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleUploadFile handles native upload of files
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
 | 
				
			||||||
 | 
						if b.mc6 != nil {
 | 
				
			||||||
 | 
							return b.handleUploadFile6(msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						var res, id string
 | 
				
			||||||
 | 
						channelID := b.getChannelID(msg.Channel)
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							msg.Text = fi.Comment
 | 
				
			||||||
 | 
							if b.GetBool("PrefixMessagesWithNick") {
 | 
				
			||||||
 | 
								msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return res, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint:forcetypeassert,wrapcheck
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleUploadFile6(msg *config.Message) (string, error) {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						var res, id string
 | 
				
			||||||
 | 
						channelID := b.getChannelID(msg.Channel)
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							id, err = b.mc6.UploadFile(*fi.Data, channelID, fi.Name)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							msg.Text = fi.Comment
 | 
				
			||||||
 | 
							if b.GetBool("PrefixMessagesWithNick") {
 | 
				
			||||||
 | 
								msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							res, err = b.mc6.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return res, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
 | 
				
			||||||
 | 
						props := message.Post.Props
 | 
				
			||||||
 | 
						if props == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, ok := props["override_username"].(string); ok {
 | 
				
			||||||
 | 
							rmsg.Username = props["override_username"].(string)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, ok := props["attachments"].([]interface{}); ok {
 | 
				
			||||||
 | 
							rmsg.Extra["attachments"] = props["attachments"].([]interface{})
 | 
				
			||||||
 | 
							if rmsg.Text == "" {
 | 
				
			||||||
 | 
								for _, attachment := range rmsg.Extra["attachments"] {
 | 
				
			||||||
 | 
									attach := attachment.(map[string]interface{})
 | 
				
			||||||
 | 
									if attach["text"].(string) != "" {
 | 
				
			||||||
 | 
										rmsg.Text += attach["text"].(string)
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if attach["fallback"].(string) != "" {
 | 
				
			||||||
 | 
										rmsg.Text += attach["fallback"].(string)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint:forcetypeassert
 | 
				
			||||||
 | 
					func (b *Bmattermost) handleProps6(rmsg *config.Message, message *matterclient6.Message) {
 | 
				
			||||||
 | 
						props := message.Post.Props
 | 
				
			||||||
 | 
						if props == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, ok := props["override_username"].(string); ok {
 | 
				
			||||||
 | 
							rmsg.Username = props["override_username"].(string)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, ok := props["attachments"].([]interface{}); ok {
 | 
				
			||||||
 | 
							rmsg.Extra["attachments"] = props["attachments"].([]interface{})
 | 
				
			||||||
 | 
							if rmsg.Text != "" {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, attachment := range rmsg.Extra["attachments"] {
 | 
				
			||||||
 | 
								attach := attachment.(map[string]interface{})
 | 
				
			||||||
 | 
								if attach["text"].(string) != "" {
 | 
				
			||||||
 | 
									rmsg.Text += attach["text"].(string)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if attach["fallback"].(string) != "" {
 | 
				
			||||||
 | 
									rmsg.Text += attach["fallback"].(string)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										418
									
								
								bridge/mattermost/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										418
									
								
								bridge/mattermost/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,418 @@
 | 
				
			|||||||
 | 
					package bmattermost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterclient"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterhook"
 | 
				
			||||||
 | 
						matterclient6 "github.com/matterbridge/matterclient"
 | 
				
			||||||
 | 
						"github.com/mattermost/mattermost-server/v5/model"
 | 
				
			||||||
 | 
						model6 "github.com/mattermost/mattermost-server/v6/model"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) doConnectWebhookBind() error {
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case b.GetString("WebhookURL") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
 | 
				
			||||||
 | 
							b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
				
			||||||
 | 
								matterhook.Config{
 | 
				
			||||||
 | 
									InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
				
			||||||
 | 
									BindAddress:        b.GetString("WebhookBindAddress"),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
						case b.GetString("Token") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using token (sending)")
 | 
				
			||||||
 | 
							b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.v6 {
 | 
				
			||||||
 | 
								err := b.apiLogin6()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								err := b.apiLogin()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case b.GetString("Login") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using login/password (sending)")
 | 
				
			||||||
 | 
							b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.v6 {
 | 
				
			||||||
 | 
								err := b.apiLogin6()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								err := b.apiLogin()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using webhookbindaddress (receiving)")
 | 
				
			||||||
 | 
							b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
				
			||||||
 | 
								matterhook.Config{
 | 
				
			||||||
 | 
									InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
				
			||||||
 | 
									BindAddress:        b.GetString("WebhookBindAddress"),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) doConnectWebhookURL() error {
 | 
				
			||||||
 | 
						b.Log.Info("Connecting using webhookurl (sending)")
 | 
				
			||||||
 | 
						b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
				
			||||||
 | 
							matterhook.Config{
 | 
				
			||||||
 | 
								InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
				
			||||||
 | 
								DisableServer:      true,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						if b.GetString("Token") != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using token (receiving)")
 | 
				
			||||||
 | 
							b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.v6 {
 | 
				
			||||||
 | 
								err := b.apiLogin6()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								err := b.apiLogin()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if b.GetString("Login") != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using login/password (receiving)")
 | 
				
			||||||
 | 
							b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.v6 {
 | 
				
			||||||
 | 
								err := b.apiLogin6()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								err := b.apiLogin()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) apiLogin() error {
 | 
				
			||||||
 | 
						password := b.GetString("Password")
 | 
				
			||||||
 | 
						if b.GetString("Token") != "" {
 | 
				
			||||||
 | 
							password = "token=" + b.GetString("Token")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
 | 
				
			||||||
 | 
						if b.GetBool("debug") {
 | 
				
			||||||
 | 
							b.mc.SetLogLevel("debug")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
 | 
				
			||||||
 | 
						b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
 | 
				
			||||||
 | 
						b.mc.NoTLS = b.GetBool("NoTLS")
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
 | 
				
			||||||
 | 
						err := b.mc.Login()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						b.TeamID = b.mc.GetTeamId()
 | 
				
			||||||
 | 
						go b.mc.WsReceiver()
 | 
				
			||||||
 | 
						go b.mc.StatusLoop()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint:wrapcheck
 | 
				
			||||||
 | 
					func (b *Bmattermost) apiLogin6() error {
 | 
				
			||||||
 | 
						password := b.GetString("Password")
 | 
				
			||||||
 | 
						if b.GetString("Token") != "" {
 | 
				
			||||||
 | 
							password = "token=" + b.GetString("Token")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.mc6 = matterclient6.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
 | 
				
			||||||
 | 
						if b.GetBool("debug") {
 | 
				
			||||||
 | 
							b.mc6.SetLogLevel("debug")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.mc6.SkipTLSVerify = b.GetBool("SkipTLSVerify")
 | 
				
			||||||
 | 
						b.mc6.SkipVersionCheck = b.GetBool("SkipVersionCheck")
 | 
				
			||||||
 | 
						b.mc6.NoTLS = b.GetBool("NoTLS")
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := b.mc6.Login(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						b.TeamID = b.mc6.GetTeamID()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// replaceAction replace the message with the correct action (/me) code
 | 
				
			||||||
 | 
					func (b *Bmattermost) replaceAction(text string) (string, bool) {
 | 
				
			||||||
 | 
						if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
 | 
				
			||||||
 | 
							return strings.Replace(text, "*", "", -1), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return text, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) {
 | 
				
			||||||
 | 
						fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
						/* if we have a sha we have successfully uploaded the file to the media server,
 | 
				
			||||||
 | 
						so we can now cache the sha */
 | 
				
			||||||
 | 
						if fi.SHA != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
 | 
				
			||||||
 | 
							b.avatarMap[msg.UserID] = fi.SHA
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// sendWebhook uses the configured WebhookURL to send the message
 | 
				
			||||||
 | 
					func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// skip events
 | 
				
			||||||
 | 
						if msg.Event != "" {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("PrefixMessagesWithNick") {
 | 
				
			||||||
 | 
							msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								rmsg := rmsg // scopelint
 | 
				
			||||||
 | 
								iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
 | 
				
			||||||
 | 
								matterMessage := matterhook.OMessage{
 | 
				
			||||||
 | 
									IconURL:  iconURL,
 | 
				
			||||||
 | 
									Channel:  rmsg.Channel,
 | 
				
			||||||
 | 
									UserName: rmsg.Username,
 | 
				
			||||||
 | 
									Text:     rmsg.Text,
 | 
				
			||||||
 | 
									Props:    make(map[string]interface{}),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								matterMessage.Props["matterbridge_"+b.uuid] = true
 | 
				
			||||||
 | 
								if err := b.mh.Send(matterMessage); err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("sendWebhook failed: %s ", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// webhook doesn't support file uploads, so we add the url manually
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
									fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
									if fi.URL != "" {
 | 
				
			||||||
 | 
										msg.Text += " " + fi.URL
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
 | 
				
			||||||
 | 
						matterMessage := matterhook.OMessage{
 | 
				
			||||||
 | 
							IconURL:  iconURL,
 | 
				
			||||||
 | 
							Channel:  msg.Channel,
 | 
				
			||||||
 | 
							UserName: msg.Username,
 | 
				
			||||||
 | 
							Text:     msg.Text,
 | 
				
			||||||
 | 
							Props:    make(map[string]interface{}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if msg.Avatar != "" {
 | 
				
			||||||
 | 
							matterMessage.IconURL = msg.Avatar
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						matterMessage.Props["matterbridge_"+b.uuid] = true
 | 
				
			||||||
 | 
						err := b.mh.Send(matterMessage)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Info(err)
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// skipMessages returns true if this message should not be handled
 | 
				
			||||||
 | 
					func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
 | 
				
			||||||
 | 
						// Handle join/leave
 | 
				
			||||||
 | 
						if message.Type == "system_join_leave" ||
 | 
				
			||||||
 | 
							message.Type == "system_join_channel" ||
 | 
				
			||||||
 | 
							message.Type == "system_leave_channel" {
 | 
				
			||||||
 | 
							if b.GetBool("nosendjoinpart") {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							channelName := b.getChannelName(message.Post.ChannelId)
 | 
				
			||||||
 | 
							if channelName == "" {
 | 
				
			||||||
 | 
								channelName = message.Channel
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
				
			||||||
 | 
							b.Remote <- config.Message{
 | 
				
			||||||
 | 
								Username: "system",
 | 
				
			||||||
 | 
								Text:     message.Text,
 | 
				
			||||||
 | 
								Channel:  channelName,
 | 
				
			||||||
 | 
								Account:  b.Account,
 | 
				
			||||||
 | 
								Event:    config.EventJoinLeave,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle edited messages
 | 
				
			||||||
 | 
						if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore non-post messages
 | 
				
			||||||
 | 
						if message.Post == nil {
 | 
				
			||||||
 | 
							b.Log.Debugf("ignoring nil message.Post: %#v", message)
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore messages sent from matterbridge
 | 
				
			||||||
 | 
						if message.Post.Props != nil {
 | 
				
			||||||
 | 
							if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
 | 
				
			||||||
 | 
								b.Log.Debugf("sent by matterbridge, ignoring")
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore messages sent from a user logged in as the bot
 | 
				
			||||||
 | 
						if b.mc.User.Username == message.Username {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
 | 
				
			||||||
 | 
						if message.Post.HasReactions {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ignore messages from other teams than ours
 | 
				
			||||||
 | 
						if message.Raw.Data["team_id"].(string) != b.TeamID {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// only handle posted, edited or deleted events
 | 
				
			||||||
 | 
						if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED ||
 | 
				
			||||||
 | 
							message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// skipMessages returns true if this message should not be handled
 | 
				
			||||||
 | 
					// nolint:gocyclo,cyclop
 | 
				
			||||||
 | 
					func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
 | 
				
			||||||
 | 
						// Handle join/leave
 | 
				
			||||||
 | 
						if message.Type == "system_join_leave" ||
 | 
				
			||||||
 | 
							message.Type == "system_join_channel" ||
 | 
				
			||||||
 | 
							message.Type == "system_leave_channel" {
 | 
				
			||||||
 | 
							if b.GetBool("nosendjoinpart") {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							channelName := b.getChannelName(message.Post.ChannelId)
 | 
				
			||||||
 | 
							if channelName == "" {
 | 
				
			||||||
 | 
								channelName = message.Channel
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
				
			||||||
 | 
							b.Remote <- config.Message{
 | 
				
			||||||
 | 
								Username: "system",
 | 
				
			||||||
 | 
								Text:     message.Text,
 | 
				
			||||||
 | 
								Channel:  channelName,
 | 
				
			||||||
 | 
								Account:  b.Account,
 | 
				
			||||||
 | 
								Event:    config.EventJoinLeave,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle edited messages
 | 
				
			||||||
 | 
						if (message.Raw.EventType() == model6.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore non-post messages
 | 
				
			||||||
 | 
						if message.Post == nil {
 | 
				
			||||||
 | 
							b.Log.Debugf("ignoring nil message.Post: %#v", message)
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore messages sent from matterbridge
 | 
				
			||||||
 | 
						if message.Post.Props != nil {
 | 
				
			||||||
 | 
							if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
 | 
				
			||||||
 | 
								b.Log.Debug("sent by matterbridge, ignoring")
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore messages sent from a user logged in as the bot
 | 
				
			||||||
 | 
						if b.mc6.User.Username == message.Username {
 | 
				
			||||||
 | 
							b.Log.Debug("message from same user as bot, ignoring")
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
 | 
				
			||||||
 | 
						if message.Post.HasReactions {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ignore messages from other teams than ours
 | 
				
			||||||
 | 
						if message.Raw.GetData()["team_id"].(string) != b.TeamID {
 | 
				
			||||||
 | 
							b.Log.Debug("message from other team, ignoring")
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// only handle posted, edited or deleted events
 | 
				
			||||||
 | 
						if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model6.WebsocketEventPostEdited ||
 | 
				
			||||||
 | 
							message.Raw.EventType() == model6.WebsocketEventPostDeleted) {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) getVersion() string {
 | 
				
			||||||
 | 
						proto := "https"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("notls") {
 | 
				
			||||||
 | 
							proto = "http"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := http.Get(proto + "://" + b.GetString("server"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Error("failed getting version")
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return resp.Header.Get("X-Version-Id")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) getChannelID(name string) string {
 | 
				
			||||||
 | 
						idcheck := strings.Split(name, "ID:")
 | 
				
			||||||
 | 
						if len(idcheck) > 1 {
 | 
				
			||||||
 | 
							return idcheck[1]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.mc6 != nil {
 | 
				
			||||||
 | 
							return b.mc6.GetChannelID(name, b.TeamID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.mc.GetChannelId(name, b.TeamID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) getChannelName(id string) string {
 | 
				
			||||||
 | 
						b.channelsMutex.RLock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, c := range b.channelInfoMap {
 | 
				
			||||||
 | 
							if c.Name == "ID:"+id {
 | 
				
			||||||
 | 
								// if we have ID: specified in our gateway configuration return this
 | 
				
			||||||
 | 
								return c.Name
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										243
									
								
								bridge/mattermost/mattermost.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								bridge/mattermost/mattermost.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,243 @@
 | 
				
			|||||||
 | 
					package bmattermost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterclient"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterhook"
 | 
				
			||||||
 | 
						matterclient6 "github.com/matterbridge/matterclient"
 | 
				
			||||||
 | 
						"github.com/rs/xid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bmattermost struct {
 | 
				
			||||||
 | 
						mh     *matterhook.Client
 | 
				
			||||||
 | 
						mc     *matterclient.MMClient
 | 
				
			||||||
 | 
						mc6    *matterclient6.Client
 | 
				
			||||||
 | 
						v6     bool
 | 
				
			||||||
 | 
						uuid   string
 | 
				
			||||||
 | 
						TeamID string
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
						avatarMap      map[string]string
 | 
				
			||||||
 | 
						channelsMutex  sync.RWMutex
 | 
				
			||||||
 | 
						channelInfoMap map[string]*config.ChannelInfo
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mattermostPlugin = "mattermost.plugin"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &Bmattermost{
 | 
				
			||||||
 | 
							Config:         cfg,
 | 
				
			||||||
 | 
							avatarMap:      make(map[string]string),
 | 
				
			||||||
 | 
							channelInfoMap: make(map[string]*config.ChannelInfo),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.v6 = b.GetBool("v6")
 | 
				
			||||||
 | 
						b.uuid = xid.New().String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) Command(cmd string) string {
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) Connect() error {
 | 
				
			||||||
 | 
						if b.Account == mattermostPlugin {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") {
 | 
				
			||||||
 | 
							if !b.v6 {
 | 
				
			||||||
 | 
								b.v6 = true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetString("WebhookBindAddress") != "" {
 | 
				
			||||||
 | 
							if err := b.doConnectWebhookBind(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleMatter()
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case b.GetString("WebhookURL") != "":
 | 
				
			||||||
 | 
							if err := b.doConnectWebhookURL(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleMatter()
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						case b.GetString("Token") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using token (sending and receiving)")
 | 
				
			||||||
 | 
							b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.v6 {
 | 
				
			||||||
 | 
								err := b.apiLogin6()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								err := b.apiLogin()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleMatter()
 | 
				
			||||||
 | 
						case b.GetString("Login") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using login/password (sending and receiving)")
 | 
				
			||||||
 | 
							b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.v6 {
 | 
				
			||||||
 | 
								err := b.apiLogin6()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								err := b.apiLogin()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleMatter()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
 | 
				
			||||||
 | 
							b.GetString("Login") == "" && b.GetString("Token") == "" {
 | 
				
			||||||
 | 
							return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						if b.Account == mattermostPlugin {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.channelsMutex.Lock()
 | 
				
			||||||
 | 
						b.channelInfoMap[channel.ID] = &channel
 | 
				
			||||||
 | 
						b.channelsMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we can only join channels using the API
 | 
				
			||||||
 | 
						if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
 | 
				
			||||||
 | 
							id := b.getChannelID(channel.Name)
 | 
				
			||||||
 | 
							if id == "" {
 | 
				
			||||||
 | 
								return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.mc6 != nil {
 | 
				
			||||||
 | 
								return b.mc6.JoinChannel(id) // nolint:wrapcheck
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return b.mc.JoinChannel(id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmattermost) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						if b.Account == mattermostPlugin {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make a action /me of the message
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
							msg.Text = "*" + msg.Text + "*"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// map the file SHA to our user (caches the avatar)
 | 
				
			||||||
 | 
						if msg.Event == config.EventAvatarDownload {
 | 
				
			||||||
 | 
							return b.cacheAvatar(&msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use webhook to send the message
 | 
				
			||||||
 | 
						if b.GetString("WebhookURL") != "" {
 | 
				
			||||||
 | 
							return b.sendWebhook(msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if b.mc6 != nil {
 | 
				
			||||||
 | 
								return msg.ID, b.mc6.DeleteMessage(msg.ID) // nolint:wrapcheck
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return msg.ID, b.mc.DeleteMessage(msg.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle prefix hint for unthreaded messages.
 | 
				
			||||||
 | 
						if msg.ParentNotFound() {
 | 
				
			||||||
 | 
							msg.ParentID = ""
 | 
				
			||||||
 | 
							msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
 | 
				
			||||||
 | 
						if msg.ParentID != "" {
 | 
				
			||||||
 | 
							if b.mc6 != nil {
 | 
				
			||||||
 | 
								post, _, err := b.mc6.Client.GetPost(msg.ParentID, "")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if post.RootId != "" {
 | 
				
			||||||
 | 
									msg.ParentID = post.RootId
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								post, res := b.mc.Client.GetPost(msg.ParentID, "")
 | 
				
			||||||
 | 
								if res.Error != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("getting post %s failed: %s", msg.ParentID, res.Error.DetailedError)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if post.RootId != "" {
 | 
				
			||||||
 | 
									msg.ParentID = post.RootId
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file if it exists
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								if b.mc6 != nil {
 | 
				
			||||||
 | 
									if _, err := b.mc6.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
 | 
				
			||||||
 | 
										b.Log.Errorf("PostMessage failed: %s", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
 | 
				
			||||||
 | 
										b.Log.Errorf("PostMessage failed: %s", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return b.handleUploadFile(&msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Prepend nick if configured
 | 
				
			||||||
 | 
						if b.GetBool("PrefixMessagesWithNick") {
 | 
				
			||||||
 | 
							msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message if we have an ID
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							if b.mc6 != nil {
 | 
				
			||||||
 | 
								return b.mc6.EditMessage(msg.ID, msg.Text) // nolint:wrapcheck
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return b.mc.EditMessage(msg.ID, msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post normal message
 | 
				
			||||||
 | 
						if b.mc6 != nil {
 | 
				
			||||||
 | 
							return b.mc6.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID) // nolint:wrapcheck
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										101
									
								
								bridge/msteams/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								bridge/msteams/handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
				
			|||||||
 | 
					package bmsteams
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msgraph "github.com/yaegashi/msgraph.go/beta"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) findFile(weburl string) (string, error) {
 | 
				
			||||||
 | 
						itemRB, err := b.gc.GetDriveItemByURL(b.ctx, weburl)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						itemRB.Workbook().Worksheets()
 | 
				
			||||||
 | 
						b.gc.Workbooks()
 | 
				
			||||||
 | 
						item, err := itemRB.Request().Get(b.ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if url, ok := item.GetAdditionalData("@microsoft.graph.downloadUrl"); ok {
 | 
				
			||||||
 | 
							return url.(string), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadFile handles file download
 | 
				
			||||||
 | 
					func (b *Bmsteams) handleDownloadFile(rmsg *config.Message, filename, weburl string) error {
 | 
				
			||||||
 | 
						realURL, err := b.findFile(weburl)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Actually download the file.
 | 
				
			||||||
 | 
						data, err := helper.DownloadFile(realURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("download %s failed %#v", weburl, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If a comment is attached to the file(s) it is in the 'Text' field of the teams messge event
 | 
				
			||||||
 | 
						// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
 | 
				
			||||||
 | 
						// that the comment is not duplicated.
 | 
				
			||||||
 | 
						comment := rmsg.Text
 | 
				
			||||||
 | 
						rmsg.Text = ""
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, rmsg, filename, comment, weburl, data, b.General)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) handleAttachments(rmsg *config.Message, msg msgraph.ChatMessage) {
 | 
				
			||||||
 | 
						for _, a := range msg.Attachments {
 | 
				
			||||||
 | 
							//remove the attachment tags from the text
 | 
				
			||||||
 | 
							rmsg.Text = attachRE.ReplaceAllString(rmsg.Text, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//handle a code snippet (code block)
 | 
				
			||||||
 | 
							if *a.ContentType == "application/vnd.microsoft.card.codesnippet" {
 | 
				
			||||||
 | 
								b.handleCodeSnippet(rmsg, a)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//handle the download
 | 
				
			||||||
 | 
							err := b.handleDownloadFile(rmsg, *a.Name, *a.ContentURL)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("download of %s failed: %s", *a.Name, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AttachContent struct {
 | 
				
			||||||
 | 
						Language       string `json:"language"`
 | 
				
			||||||
 | 
						CodeSnippetURL string `json:"codeSnippetUrl"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) handleCodeSnippet(rmsg *config.Message, attach msgraph.ChatMessageAttachment) {
 | 
				
			||||||
 | 
						var content AttachContent
 | 
				
			||||||
 | 
						err := json.Unmarshal([]byte(*attach.Content), &content)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("unmarshal codesnippet failed: %s", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						s := strings.Split(content.CodeSnippetURL, "/")
 | 
				
			||||||
 | 
						if len(s) != 13 {
 | 
				
			||||||
 | 
							b.Log.Errorf("codesnippetUrl has unexpected size: %s", content.CodeSnippetURL)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resp, err := b.gc.Teams().Request().Client().Get(content.CodeSnippetURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("retrieving snippet content failed:%s", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						res, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("reading snippet data failed: %s", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rmsg.Text = rmsg.Text + "\n```" + content.Language + "\n" + string(res) + "\n```\n"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										229
									
								
								bridge/msteams/msteams.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								bridge/msteams/msteams.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,229 @@
 | 
				
			|||||||
 | 
					package bmsteams
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/davecgh/go-spew/spew"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/mattn/godown"
 | 
				
			||||||
 | 
						msgraph "github.com/yaegashi/msgraph.go/beta"
 | 
				
			||||||
 | 
						"github.com/yaegashi/msgraph.go/msauth"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/oauth2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
 | 
				
			||||||
 | 
						attachRE      = regexp.MustCompile(`<attachment id=.*?attachment>`)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bmsteams struct {
 | 
				
			||||||
 | 
						gc    *msgraph.GraphServiceRequestBuilder
 | 
				
			||||||
 | 
						ctx   context.Context
 | 
				
			||||||
 | 
						botID string
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						return &Bmsteams{Config: cfg}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) Connect() error {
 | 
				
			||||||
 | 
						tokenCachePath := b.GetString("sessionFile")
 | 
				
			||||||
 | 
						if tokenCachePath == "" {
 | 
				
			||||||
 | 
							tokenCachePath = "msteams_session.json"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
						m := msauth.NewManager()
 | 
				
			||||||
 | 
						m.LoadFile(tokenCachePath) //nolint:errcheck
 | 
				
			||||||
 | 
						ts, err := m.DeviceAuthorizationGrant(ctx, b.GetString("TenantID"), b.GetString("ClientID"), defaultScopes, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = m.SaveFile(tokenCachePath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// make file readable only for matterbridge user
 | 
				
			||||||
 | 
						err = os.Chmod(tokenCachePath, 0o600)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						httpClient := oauth2.NewClient(ctx, ts)
 | 
				
			||||||
 | 
						graphClient := msgraph.NewClient(httpClient)
 | 
				
			||||||
 | 
						b.gc = graphClient
 | 
				
			||||||
 | 
						b.ctx = ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = b.setBotID()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						go func(name string) {
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								err := b.poll(name)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								time.Sleep(time.Second * 5)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}(channel.Name)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
						if msg.ParentValid() {
 | 
				
			||||||
 | 
							return b.sendReply(msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle prefix hint for unthreaded messages.
 | 
				
			||||||
 | 
						if msg.ParentNotFound() {
 | 
				
			||||||
 | 
							msg.ParentID = ""
 | 
				
			||||||
 | 
							msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
 | 
				
			||||||
 | 
						text := msg.Username + msg.Text
 | 
				
			||||||
 | 
						content := &msgraph.ItemBody{Content: &text}
 | 
				
			||||||
 | 
						rmsg := &msgraph.ChatMessage{Body: content}
 | 
				
			||||||
 | 
						res, err := ct.Add(b.ctx, rmsg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return *res.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) sendReply(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().ID(msg.ParentID).Replies().Request()
 | 
				
			||||||
 | 
						// Handle prefix hint for unthreaded messages.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						text := msg.Username + msg.Text
 | 
				
			||||||
 | 
						content := &msgraph.ItemBody{Content: &text}
 | 
				
			||||||
 | 
						rmsg := &msgraph.ChatMessage{Body: content}
 | 
				
			||||||
 | 
						res, err := ct.Add(b.ctx, rmsg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return *res.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) getMessages(channel string) ([]msgraph.ChatMessage, error) {
 | 
				
			||||||
 | 
						ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(channel).Messages().Request()
 | 
				
			||||||
 | 
						rct, err := ct.Get(b.ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("got %#v messages", len(rct))
 | 
				
			||||||
 | 
						return rct, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//nolint:gocognit
 | 
				
			||||||
 | 
					func (b *Bmsteams) poll(channelName string) error {
 | 
				
			||||||
 | 
						msgmap := make(map[string]time.Time)
 | 
				
			||||||
 | 
						b.Log.Debug("getting initial messages")
 | 
				
			||||||
 | 
						res, err := b.getMessages(channelName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, msg := range res {
 | 
				
			||||||
 | 
							msgmap[*msg.ID] = *msg.CreatedDateTime
 | 
				
			||||||
 | 
							if msg.LastModifiedDateTime != nil {
 | 
				
			||||||
 | 
								msgmap[*msg.ID] = *msg.LastModifiedDateTime
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						time.Sleep(time.Second * 5)
 | 
				
			||||||
 | 
						b.Log.Debug("polling for messages")
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							res, err := b.getMessages(channelName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for i := len(res) - 1; i >= 0; i-- {
 | 
				
			||||||
 | 
								msg := res[i]
 | 
				
			||||||
 | 
								if mtime, ok := msgmap[*msg.ID]; ok {
 | 
				
			||||||
 | 
									if mtime == *msg.CreatedDateTime && msg.LastModifiedDateTime == nil {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if msg.LastModifiedDateTime != nil && mtime == *msg.LastModifiedDateTime {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if b.GetBool("debug") {
 | 
				
			||||||
 | 
									b.Log.Debug("Msg dump: ", spew.Sdump(msg))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// skip non-user message for now.
 | 
				
			||||||
 | 
								if msg.From == nil || msg.From.User == nil {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if *msg.From.User.ID == b.botID {
 | 
				
			||||||
 | 
									b.Log.Debug("skipping own message")
 | 
				
			||||||
 | 
									msgmap[*msg.ID] = *msg.CreatedDateTime
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								msgmap[*msg.ID] = *msg.CreatedDateTime
 | 
				
			||||||
 | 
								if msg.LastModifiedDateTime != nil {
 | 
				
			||||||
 | 
									msgmap[*msg.ID] = *msg.LastModifiedDateTime
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Sending message from %s on %s to gateway", *msg.From.User.DisplayName, b.Account)
 | 
				
			||||||
 | 
								text := b.convertToMD(*msg.Body.Content)
 | 
				
			||||||
 | 
								rmsg := config.Message{
 | 
				
			||||||
 | 
									Username: *msg.From.User.DisplayName,
 | 
				
			||||||
 | 
									Text:     text,
 | 
				
			||||||
 | 
									Channel:  channelName,
 | 
				
			||||||
 | 
									Account:  b.Account,
 | 
				
			||||||
 | 
									Avatar:   "",
 | 
				
			||||||
 | 
									UserID:   *msg.From.User.ID,
 | 
				
			||||||
 | 
									ID:       *msg.ID,
 | 
				
			||||||
 | 
									Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								b.handleAttachments(&rmsg, msg)
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
								b.Remote <- rmsg
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							time.Sleep(time.Second * 5)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) setBotID() error {
 | 
				
			||||||
 | 
						req := b.gc.Me().Request()
 | 
				
			||||||
 | 
						r, err := req.Get(b.ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.botID = *r.ID
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmsteams) convertToMD(text string) string {
 | 
				
			||||||
 | 
						if !strings.Contains(text, "<div>") {
 | 
				
			||||||
 | 
							return text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var sb strings.Builder
 | 
				
			||||||
 | 
						err := godown.Convert(&sb, strings.NewReader(text), nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Couldn't convert message to markdown %s", text)
 | 
				
			||||||
 | 
							return text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return sb.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										70
									
								
								bridge/mumble/codec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								bridge/mumble/codec.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					package bmumble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"layeh.com/gumble/gumble"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This is a dummy implementation of a Gumble audio codec which claims
 | 
				
			||||||
 | 
					// to implement Opus, but does not actually do anything.  This serves
 | 
				
			||||||
 | 
					// as a workaround until https://github.com/layeh/gumble/pull/61 is
 | 
				
			||||||
 | 
					// merged.
 | 
				
			||||||
 | 
					// See https://github.com/42wim/matterbridge/issues/1750 for details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						audioCodecIDOpus = 4
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func registerNullCodecAsOpus() {
 | 
				
			||||||
 | 
						codec := &NullCodec{
 | 
				
			||||||
 | 
							encoder: &NullAudioEncoder{},
 | 
				
			||||||
 | 
							decoder: &NullAudioDecoder{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						gumble.RegisterAudioCodec(audioCodecIDOpus, codec)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type NullCodec struct {
 | 
				
			||||||
 | 
						encoder *NullAudioEncoder
 | 
				
			||||||
 | 
						decoder *NullAudioDecoder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *NullCodec) ID() int {
 | 
				
			||||||
 | 
						return audioCodecIDOpus
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *NullCodec) NewEncoder() gumble.AudioEncoder {
 | 
				
			||||||
 | 
						e := &NullAudioEncoder{}
 | 
				
			||||||
 | 
						return e
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *NullCodec) NewDecoder() gumble.AudioDecoder {
 | 
				
			||||||
 | 
						d := &NullAudioDecoder{}
 | 
				
			||||||
 | 
						return d
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type NullAudioEncoder struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *NullAudioEncoder) ID() int {
 | 
				
			||||||
 | 
						return audioCodecIDOpus
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *NullAudioEncoder) Encode(pcm []int16, mframeSize, maxDataBytes int) ([]byte, error) {
 | 
				
			||||||
 | 
						return nil, fmt.Errorf("not implemented")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *NullAudioEncoder) Reset() {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type NullAudioDecoder struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (d *NullAudioDecoder) ID() int {
 | 
				
			||||||
 | 
						return audioCodecIDOpus
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (d *NullAudioDecoder) Decode(data []byte, frameSize int) ([]int16, error) {
 | 
				
			||||||
 | 
						return nil, fmt.Errorf("not implemented")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (d *NullAudioDecoder) Reset() {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								bridge/mumble/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								bridge/mumble/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					package bmumble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"layeh.com/gumble/gumble"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
 | 
				
			||||||
 | 
						b.serverConfigUpdate <- *event
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
 | 
				
			||||||
 | 
						sender := "unknown"
 | 
				
			||||||
 | 
						if event.TextMessage.Sender != nil {
 | 
				
			||||||
 | 
							sender = event.TextMessage.Sender.Name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// If the text message is received before receiving a ServerSync
 | 
				
			||||||
 | 
						// and UserState, Client.Self or Self.Channel are nil
 | 
				
			||||||
 | 
						if event.Client.Self == nil || event.Client.Self.Channel == nil {
 | 
				
			||||||
 | 
							b.Log.Warn("Connection bootstrap not finished, discarding text message")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Convert Mumble HTML messages to markdown
 | 
				
			||||||
 | 
						parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Error(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						now := time.Now().UTC()
 | 
				
			||||||
 | 
						for i, part := range parts {
 | 
				
			||||||
 | 
							// Construct matterbridge message and pass on to the gateway
 | 
				
			||||||
 | 
							rmsg := config.Message{
 | 
				
			||||||
 | 
								Channel:  strconv.FormatUint(uint64(event.Client.Self.Channel.ID), 10),
 | 
				
			||||||
 | 
								Username: sender,
 | 
				
			||||||
 | 
								UserID:   sender + "@" + b.Host,
 | 
				
			||||||
 | 
								Account:  b.Account,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if part.Image == nil {
 | 
				
			||||||
 | 
								rmsg.Text = part.Text
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension
 | 
				
			||||||
 | 
								rmsg.Extra = make(map[string][]interface{})
 | 
				
			||||||
 | 
								if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil {
 | 
				
			||||||
 | 
									b.Log.WithError(err).Warn("not including image in message")
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								helper.HandleDownloadData(b.Log, &rmsg, fname, "", "", &part.Image, b.General)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Log.Debugf("Sending message to gateway: %+v", rmsg)
 | 
				
			||||||
 | 
							b.Remote <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
 | 
				
			||||||
 | 
						// Set the user's "bio"/comment
 | 
				
			||||||
 | 
						if comment := b.GetString("UserComment"); comment != "" && event.Client.Self != nil {
 | 
				
			||||||
 | 
							event.Client.Self.SetComment(comment)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// No need to talk or listen
 | 
				
			||||||
 | 
						event.Client.Self.SetSelfDeafened(true)
 | 
				
			||||||
 | 
						event.Client.Self.SetSelfMuted(true)
 | 
				
			||||||
 | 
						// if the Channel variable is set, this is a reconnect -> rejoin channel
 | 
				
			||||||
 | 
						if b.Channel != nil {
 | 
				
			||||||
 | 
							if err := b.doJoin(event.Client, *b.Channel); err != nil {
 | 
				
			||||||
 | 
								b.Log.Error(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Remote <- config.Message{
 | 
				
			||||||
 | 
								Username: "system",
 | 
				
			||||||
 | 
								Text:     "rejoin",
 | 
				
			||||||
 | 
								Channel:  "",
 | 
				
			||||||
 | 
								Account:  b.Account,
 | 
				
			||||||
 | 
								Event:    config.EventRejoinChannels,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
 | 
				
			||||||
 | 
						// Only care about changes to self
 | 
				
			||||||
 | 
						if event.User != event.Client.Self {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Someone attempted to move the user out of the configured channel; attempt to join back
 | 
				
			||||||
 | 
						if b.Channel != nil {
 | 
				
			||||||
 | 
							if err := b.doJoin(event.Client, *b.Channel); err != nil {
 | 
				
			||||||
 | 
								b.Log.Error(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
 | 
				
			||||||
 | 
						b.connected <- *event
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										143
									
								
								bridge/mumble/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								bridge/mumble/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
				
			|||||||
 | 
					package bmumble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/mattn/godown"
 | 
				
			||||||
 | 
						"github.com/vincent-petithory/dataurl"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MessagePart struct {
 | 
				
			||||||
 | 
						Text          string
 | 
				
			||||||
 | 
						FileExtension string
 | 
				
			||||||
 | 
						Image         []byte
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) decodeImage(uri string, parts *[]MessagePart) error {
 | 
				
			||||||
 | 
						// Decode the data:image/... URI
 | 
				
			||||||
 | 
						image, err := dataurl.DecodeString(uri)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.WithError(err).Info("No image extracted")
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Determine the file extensions for that image
 | 
				
			||||||
 | 
						ext, err := mime.ExtensionsByType(image.MediaType.ContentType())
 | 
				
			||||||
 | 
						if err != nil || len(ext) == 0 {
 | 
				
			||||||
 | 
							b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType())
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Add the image to the MessagePart slice
 | 
				
			||||||
 | 
						*parts = append(*parts, MessagePart{"", ext[0], image.Data})
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) {
 | 
				
			||||||
 | 
						// `^(.*?)` matches everything before the image
 | 
				
			||||||
 | 
						// `!\[[^\]]*\]\(` matches the `]+)` matches the data: URI used by Mumble
 | 
				
			||||||
 | 
						// `\)` matches the closing parenthesis after the URI
 | 
				
			||||||
 | 
						// `(.*)$` matches the remaining text to be examined in the next iteration
 | 
				
			||||||
 | 
						p := regexp.MustCompile(`^(?ms)(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`)
 | 
				
			||||||
 | 
						remaining := *t
 | 
				
			||||||
 | 
						var parts []MessagePart
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							tokens := p.FindStringSubmatch(remaining)
 | 
				
			||||||
 | 
							if tokens == nil {
 | 
				
			||||||
 | 
								// no match -> remaining string is non-image text
 | 
				
			||||||
 | 
								pre := strings.TrimSpace(remaining)
 | 
				
			||||||
 | 
								if len(pre) > 0 {
 | 
				
			||||||
 | 
									parts = append(parts, MessagePart{pre, "", nil})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return parts, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// tokens[1] is the text before the image
 | 
				
			||||||
 | 
							if len(tokens[1]) > 0 {
 | 
				
			||||||
 | 
								pre := strings.TrimSpace(tokens[1])
 | 
				
			||||||
 | 
								parts = append(parts, MessagePart{pre, "", nil})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// tokens[2] is the image URL
 | 
				
			||||||
 | 
							uri, err := dataurl.UnescapeToString(strings.TrimSpace(strings.ReplaceAll(tokens[2], " ", "")))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.WithError(err).Info("URL unescaping failed")
 | 
				
			||||||
 | 
								remaining = strings.TrimSpace(tokens[3])
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = b.decodeImage(uri, &parts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.WithError(err).Info("Decoding the image failed")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// tokens[3] is the text after the image, processed in the next iteration
 | 
				
			||||||
 | 
							remaining = strings.TrimSpace(tokens[3])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) {
 | 
				
			||||||
 | 
						var sb strings.Builder
 | 
				
			||||||
 | 
						err := godown.Convert(&sb, strings.NewReader(html), nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						markdown := sb.String()
 | 
				
			||||||
 | 
						b.Log.Debugf("### to markdown: %s", markdown)
 | 
				
			||||||
 | 
						return b.tokenize(&markdown)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) extractFiles(msg *config.Message) []config.Message {
 | 
				
			||||||
 | 
						var messages []config.Message
 | 
				
			||||||
 | 
						if msg.Extra == nil || len(msg.Extra["file"]) == 0 {
 | 
				
			||||||
 | 
							return messages
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Create a separate message for each file
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							imsg := config.Message{
 | 
				
			||||||
 | 
								Channel:   msg.Channel,
 | 
				
			||||||
 | 
								Username:  msg.Username,
 | 
				
			||||||
 | 
								UserID:    msg.UserID,
 | 
				
			||||||
 | 
								Account:   msg.Account,
 | 
				
			||||||
 | 
								Protocol:  msg.Protocol,
 | 
				
			||||||
 | 
								Timestamp: msg.Timestamp,
 | 
				
			||||||
 | 
								Event:     "mumble_image",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// If no data is present for the file, send a link instead
 | 
				
			||||||
 | 
							if fi.Data == nil || len(*fi.Data) == 0 {
 | 
				
			||||||
 | 
								if len(fi.URL) > 0 {
 | 
				
			||||||
 | 
									imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
 | 
				
			||||||
 | 
									messages = append(messages, imsg)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									b.Log.Infof("Not forwarding file without local data")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							mimeType := http.DetectContentType(*fi.Data)
 | 
				
			||||||
 | 
							// Mumble only supports images natively, send a link instead
 | 
				
			||||||
 | 
							if !strings.HasPrefix(mimeType, "image/") {
 | 
				
			||||||
 | 
								if len(fi.URL) > 0 {
 | 
				
			||||||
 | 
									imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
 | 
				
			||||||
 | 
									messages = append(messages, imsg)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									b.Log.Infof("Not forwarding file of type %s", mimeType)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
 | 
				
			||||||
 | 
							// Build data:image/...;base64,... style image URL and embed image directly into the message
 | 
				
			||||||
 | 
							du := dataurl.New(*fi.Data, mimeType)
 | 
				
			||||||
 | 
							dataURL, err := du.MarshalText()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data))
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							imsg.Text = fmt.Sprintf(`<img src="%s"/>`, dataURL)
 | 
				
			||||||
 | 
							messages = append(messages, imsg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Remove files from original message
 | 
				
			||||||
 | 
						msg.Extra["file"] = nil
 | 
				
			||||||
 | 
						return messages
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										263
									
								
								bridge/mumble/mumble.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								bridge/mumble/mumble.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,263 @@
 | 
				
			|||||||
 | 
					package bmumble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"layeh.com/gumble/gumble"
 | 
				
			||||||
 | 
						"layeh.com/gumble/gumbleutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						stripmd "github.com/writeas/go-strip-markdown"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We need to import the 'data' package as an implicit dependency.
 | 
				
			||||||
 | 
						// See: https://godoc.org/github.com/paulrosania/go-charset/charset
 | 
				
			||||||
 | 
						_ "github.com/paulrosania/go-charset/data"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bmumble struct {
 | 
				
			||||||
 | 
						client             *gumble.Client
 | 
				
			||||||
 | 
						Nick               string
 | 
				
			||||||
 | 
						Host               string
 | 
				
			||||||
 | 
						Channel            *uint32
 | 
				
			||||||
 | 
						local              chan config.Message
 | 
				
			||||||
 | 
						running            chan error
 | 
				
			||||||
 | 
						connected          chan gumble.DisconnectEvent
 | 
				
			||||||
 | 
						serverConfigUpdate chan gumble.ServerConfigEvent
 | 
				
			||||||
 | 
						serverConfig       gumble.ServerConfigEvent
 | 
				
			||||||
 | 
						tlsConfig          tls.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &Bmumble{}
 | 
				
			||||||
 | 
						b.Config = cfg
 | 
				
			||||||
 | 
						b.Nick = b.GetString("Nick")
 | 
				
			||||||
 | 
						b.local = make(chan config.Message)
 | 
				
			||||||
 | 
						b.running = make(chan error)
 | 
				
			||||||
 | 
						b.connected = make(chan gumble.DisconnectEvent)
 | 
				
			||||||
 | 
						b.serverConfigUpdate = make(chan gumble.ServerConfigEvent)
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) Connect() error {
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
				
			||||||
 | 
						host, portstr, err := net.SplitHostPort(b.GetString("Server"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Host = host
 | 
				
			||||||
 | 
						_, err = strconv.Atoi(portstr)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = b.buildTLSConfig(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go b.doSend()
 | 
				
			||||||
 | 
						go b.connectLoop()
 | 
				
			||||||
 | 
						err = <-b.running
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) Disconnect() error {
 | 
				
			||||||
 | 
						return b.client.Disconnect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						cid, err := strconv.ParseUint(channel.Name, 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						channelID := uint32(cid)
 | 
				
			||||||
 | 
						if b.Channel != nil && *b.Channel != channelID {
 | 
				
			||||||
 | 
							b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel)
 | 
				
			||||||
 | 
							return errors.New("the Mumble bridge can only join a single channel")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Channel = &channelID
 | 
				
			||||||
 | 
						return b.doJoin(b.client, channelID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// Only process text messages
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Received local message %#v", msg)
 | 
				
			||||||
 | 
						if msg.Event != "" && msg.Event != config.EventUserAction {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						attachments := b.extractFiles(&msg)
 | 
				
			||||||
 | 
						b.local <- msg
 | 
				
			||||||
 | 
						for _, a := range attachments {
 | 
				
			||||||
 | 
							b.local <- a
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) buildTLSConfig() error {
 | 
				
			||||||
 | 
						b.tlsConfig = tls.Config{}
 | 
				
			||||||
 | 
						// Load TLS client certificate keypair required for registered user authentication
 | 
				
			||||||
 | 
						if cpath := b.GetString("TLSClientCertificate"); cpath != "" {
 | 
				
			||||||
 | 
							if ckey := b.GetString("TLSClientKey"); ckey != "" {
 | 
				
			||||||
 | 
								cert, err := tls.LoadX509KeyPair(cpath, ckey)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.tlsConfig.Certificates = []tls.Certificate{cert}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Load TLS CA used for server verification.  If not provided, the Go system trust anchor is used
 | 
				
			||||||
 | 
						if capath := b.GetString("TLSCACertificate"); capath != "" {
 | 
				
			||||||
 | 
							ca, err := ioutil.ReadFile(capath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.tlsConfig.RootCAs = x509.NewCertPool()
 | 
				
			||||||
 | 
							b.tlsConfig.RootCAs.AppendCertsFromPEM(ca)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) connectLoop() {
 | 
				
			||||||
 | 
						firstConnect := true
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							err := b.doConnect()
 | 
				
			||||||
 | 
							if firstConnect {
 | 
				
			||||||
 | 
								b.running <- err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Connection to server failed: %#v", err)
 | 
				
			||||||
 | 
								if firstConnect {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									b.Log.Info("Retrying in 10s")
 | 
				
			||||||
 | 
									time.Sleep(10 * time.Second)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							firstConnect = false
 | 
				
			||||||
 | 
							d := <-b.connected
 | 
				
			||||||
 | 
							switch d.Type {
 | 
				
			||||||
 | 
							case gumble.DisconnectError:
 | 
				
			||||||
 | 
								b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							case gumble.DisconnectKicked:
 | 
				
			||||||
 | 
								b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							case gumble.DisconnectBanned:
 | 
				
			||||||
 | 
								b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String)
 | 
				
			||||||
 | 
								close(b.connected)
 | 
				
			||||||
 | 
								close(b.running)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							case gumble.DisconnectUser:
 | 
				
			||||||
 | 
								b.Log.Infof("Disconnect successful")
 | 
				
			||||||
 | 
								close(b.connected)
 | 
				
			||||||
 | 
								close(b.running)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) doConnect() error {
 | 
				
			||||||
 | 
						// Create new gumble config and attach event handlers
 | 
				
			||||||
 | 
						gumbleConfig := gumble.NewConfig()
 | 
				
			||||||
 | 
						gumbleConfig.Attach(gumbleutil.Listener{
 | 
				
			||||||
 | 
							ServerConfig: b.handleServerConfig,
 | 
				
			||||||
 | 
							TextMessage:  b.handleTextMessage,
 | 
				
			||||||
 | 
							Connect:      b.handleConnect,
 | 
				
			||||||
 | 
							Disconnect:   b.handleDisconnect,
 | 
				
			||||||
 | 
							UserChange:   b.handleUserChange,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						gumbleConfig.Username = b.GetString("Nick")
 | 
				
			||||||
 | 
						if password := b.GetString("Password"); password != "" {
 | 
				
			||||||
 | 
							gumbleConfig.Password = password
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						registerNullCodecAsOpus()
 | 
				
			||||||
 | 
						client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.client = client
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error {
 | 
				
			||||||
 | 
						channel, ok := client.Channels[channelID]
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("no channel with ID %d", channelID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						client.Self.Move(channel)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) doSend() {
 | 
				
			||||||
 | 
						// Message sending loop that makes sure server-side
 | 
				
			||||||
 | 
						// restrictions and client-side message traits don't conflict
 | 
				
			||||||
 | 
						// with each other.
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case serverConfig := <-b.serverConfigUpdate:
 | 
				
			||||||
 | 
								b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength)
 | 
				
			||||||
 | 
								b.serverConfig = serverConfig
 | 
				
			||||||
 | 
							case msg := <-b.local:
 | 
				
			||||||
 | 
								b.processMessage(&msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bmumble) processMessage(msg *config.Message) {
 | 
				
			||||||
 | 
						b.Log.Debugf("Processing message %s", msg.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allowHTML := true
 | 
				
			||||||
 | 
						if b.serverConfig.AllowHTML != nil {
 | 
				
			||||||
 | 
							allowHTML = *b.serverConfig.AllowHTML
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If this is a specially generated image message, send it unmodified
 | 
				
			||||||
 | 
						if msg.Event == "mumble_image" {
 | 
				
			||||||
 | 
							if allowHTML {
 | 
				
			||||||
 | 
								b.client.Self.Channel.Send(msg.Username+msg.Text, false)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								b.Log.Info("Can't send image, server does not allow HTML messages")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Don't process empty messages
 | 
				
			||||||
 | 
						if len(msg.Text) == 0 {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
 | 
				
			||||||
 | 
						if allowHTML {
 | 
				
			||||||
 | 
							msg.Text = helper.ParseMarkdown(msg.Text)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msg.Text = stripmd.Strip(msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If there is a maximum message length, split and truncate the lines
 | 
				
			||||||
 | 
						var msgLines []string
 | 
				
			||||||
 | 
						if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
 | 
				
			||||||
 | 
							msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Send the individual lines
 | 
				
			||||||
 | 
						for i := range msgLines {
 | 
				
			||||||
 | 
							// Remove unnecessary newline character, since either way we're sending it as individual lines
 | 
				
			||||||
 | 
							msgLines[i] = strings.TrimSuffix(msgLines[i], "\n")
 | 
				
			||||||
 | 
							b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										295
									
								
								bridge/nctalk/nctalk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								bridge/nctalk/nctalk.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,295 @@
 | 
				
			|||||||
 | 
					package nctalk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gomod.garykim.dev/nc-talk/ocs"
 | 
				
			||||||
 | 
						"gomod.garykim.dev/nc-talk/room"
 | 
				
			||||||
 | 
						"gomod.garykim.dev/nc-talk/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Btalk struct {
 | 
				
			||||||
 | 
						user  *user.TalkUser
 | 
				
			||||||
 | 
						rooms []Broom
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						return &Btalk{Config: cfg}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Broom struct {
 | 
				
			||||||
 | 
						room      *room.TalkRoom
 | 
				
			||||||
 | 
						ctx       context.Context
 | 
				
			||||||
 | 
						ctxCancel context.CancelFunc
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) Connect() error {
 | 
				
			||||||
 | 
						b.Log.Info("Connecting")
 | 
				
			||||||
 | 
						tconfig := &user.TalkUserConfig{
 | 
				
			||||||
 | 
							TLSConfig: &tls.Config{
 | 
				
			||||||
 | 
								InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Error("Config could not be used")
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = b.user.Capabilities()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Error("Cannot Connect")
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connected")
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) Disconnect() error {
 | 
				
			||||||
 | 
						for _, r := range b.rooms {
 | 
				
			||||||
 | 
							r.ctxCancel()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						tr, err := room.NewTalkRoom(b.user, channel.Name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						newRoom := Broom{
 | 
				
			||||||
 | 
							room: tr,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background())
 | 
				
			||||||
 | 
						c, err := newRoom.room.ReceiveMessages(newRoom.ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.rooms = append(b.rooms, newRoom)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							for msg := range c {
 | 
				
			||||||
 | 
								msg := msg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if msg.Error != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Ignore messages that are from the bot user
 | 
				
			||||||
 | 
								if msg.ActorID == b.user.User || msg.ActorType == "bridged" {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Handle deleting messages
 | 
				
			||||||
 | 
								if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete {
 | 
				
			||||||
 | 
									b.handleDeletingMessage(&msg, &newRoom)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Handle sending messages
 | 
				
			||||||
 | 
								if msg.MessageType == ocs.MessageComment {
 | 
				
			||||||
 | 
									b.handleSendingMessage(&msg, &newRoom)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						r := b.getRoom(msg.Channel)
 | 
				
			||||||
 | 
						if r == nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Could not find room for %v", msg.Channel)
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Standard Message Send
 | 
				
			||||||
 | 
						if msg.Event == "" {
 | 
				
			||||||
 | 
							// Handle sending files if they are included
 | 
				
			||||||
 | 
							err := b.handleSendingFile(&msg, r)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sentMessage, err := b.sendText(r, &msg, msg.Text)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return strconv.Itoa(sentMessage.ID), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Message Deletion
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							messageID, err := strconv.Atoi(msg.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							data, err := r.room.DeleteMessage(messageID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return strconv.Itoa(data.ID), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Message is not a type that is currently supported
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) getRoom(token string) *Broom {
 | 
				
			||||||
 | 
						for _, r := range b.rooms {
 | 
				
			||||||
 | 
							if r.room.Token == token {
 | 
				
			||||||
 | 
								return &r
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) {
 | 
				
			||||||
 | 
						messageToSend := &room.Message{Message: msg.Username + text}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("SeparateDisplayName") {
 | 
				
			||||||
 | 
							messageToSend.Message = text
 | 
				
			||||||
 | 
							messageToSend.ActorDisplayName = msg.Username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return r.room.SendComplexMessage(messageToSend)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error {
 | 
				
			||||||
 | 
						for _, parameter := range message.MessageParameters {
 | 
				
			||||||
 | 
							if parameter.Type == ocs.ROSTypeFile {
 | 
				
			||||||
 | 
								// Get the file
 | 
				
			||||||
 | 
								file, err := b.user.DownloadFile(parameter.Path)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if mmsg.Extra == nil {
 | 
				
			||||||
 | 
									mmsg.Extra = make(map[string][]interface{})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{
 | 
				
			||||||
 | 
									Name:   parameter.Name,
 | 
				
			||||||
 | 
									Data:   file,
 | 
				
			||||||
 | 
									Size:   int64(len(*file)),
 | 
				
			||||||
 | 
									Avatar: false,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error {
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							if fi.URL == "" {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							message := ""
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								message += fi.Comment + " "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message += fi.URL
 | 
				
			||||||
 | 
							_, err := b.sendText(r, msg, message)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
 | 
				
			||||||
 | 
						remoteMessage := config.Message{
 | 
				
			||||||
 | 
							Text:     formatRichObjectString(msg.Message, msg.MessageParameters),
 | 
				
			||||||
 | 
							Channel:  r.room.Token,
 | 
				
			||||||
 | 
							Username: DisplayName(msg, b.guestSuffix()),
 | 
				
			||||||
 | 
							UserID:   msg.ActorID,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// It is possible for the ID to not be set on older versions of Talk so we only set it if
 | 
				
			||||||
 | 
						// the ID is not blank
 | 
				
			||||||
 | 
						if msg.ID != 0 {
 | 
				
			||||||
 | 
							remoteMessage.ID = strconv.Itoa(msg.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle Files
 | 
				
			||||||
 | 
						err := b.handleFiles(&remoteMessage, msg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Error handling file: %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", remoteMessage)
 | 
				
			||||||
 | 
						b.Remote <- remoteMessage
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
 | 
				
			||||||
 | 
						remoteMessage := config.Message{
 | 
				
			||||||
 | 
							Event:   config.EventMsgDelete,
 | 
				
			||||||
 | 
							Text:    config.EventMsgDelete,
 | 
				
			||||||
 | 
							Channel: r.room.Token,
 | 
				
			||||||
 | 
							ID:      strconv.Itoa(msg.Parent.ID),
 | 
				
			||||||
 | 
							Account: b.Account,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message being deleted is %#v", remoteMessage)
 | 
				
			||||||
 | 
						b.Remote <- remoteMessage
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btalk) guestSuffix() string {
 | 
				
			||||||
 | 
						guestSuffix := " (Guest)"
 | 
				
			||||||
 | 
						if b.IsKeySet("GuestSuffix") {
 | 
				
			||||||
 | 
							guestSuffix = b.GetString("GuestSuffix")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return guestSuffix
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785
 | 
				
			||||||
 | 
					func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
 | 
				
			||||||
 | 
						for id, parameter := range parameters {
 | 
				
			||||||
 | 
							text := parameter.Name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch parameter.Type {
 | 
				
			||||||
 | 
							case ocs.ROSTypeUser, ocs.ROSTypeGroup:
 | 
				
			||||||
 | 
								text = "@" + text
 | 
				
			||||||
 | 
							case ocs.ROSTypeFile:
 | 
				
			||||||
 | 
								if parameter.Link != "" {
 | 
				
			||||||
 | 
									text = parameter.Name
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							message = strings.ReplaceAll(message, "{"+id+"}", text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return message
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string {
 | 
				
			||||||
 | 
						if msg.ActorType == ocs.ActorGuest {
 | 
				
			||||||
 | 
							if msg.ActorDisplayName == "" {
 | 
				
			||||||
 | 
								return "Guest"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return msg.ActorDisplayName + suffix
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return msg.ActorDisplayName
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										136
									
								
								bridge/rocketchat/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								bridge/rocketchat/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					package brocketchat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) handleRocket() {
 | 
				
			||||||
 | 
						messages := make(chan *config.Message)
 | 
				
			||||||
 | 
						if b.GetString("WebhookBindAddress") != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Choosing webhooks based receiving")
 | 
				
			||||||
 | 
							go b.handleRocketHook(messages)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.Log.Debugf("Choosing login/password based receiving")
 | 
				
			||||||
 | 
							go b.handleRocketClient(messages)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for message := range messages {
 | 
				
			||||||
 | 
							message.Account = b.Account
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Message is %#v", message)
 | 
				
			||||||
 | 
							b.Remote <- *message
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							message := b.rh.Receive()
 | 
				
			||||||
 | 
							b.Log.Debugf("Receiving from rockethook %#v", message)
 | 
				
			||||||
 | 
							// do not loop
 | 
				
			||||||
 | 
							if message.UserName == b.GetString("Nick") {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							messages <- &config.Message{
 | 
				
			||||||
 | 
								UserID:   message.UserID,
 | 
				
			||||||
 | 
								Username: message.UserName,
 | 
				
			||||||
 | 
								Text:     message.Text,
 | 
				
			||||||
 | 
								Channel:  message.ChannelName,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message) bool {
 | 
				
			||||||
 | 
						switch ev.Type {
 | 
				
			||||||
 | 
						case "":
 | 
				
			||||||
 | 
							// this is a normal message, no processing needed
 | 
				
			||||||
 | 
							// return true so the message is not dropped
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						case sUserJoined, sUserLeft:
 | 
				
			||||||
 | 
							rmsg.Event = config.EventJoinLeave
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						case sRoomChangedTopic:
 | 
				
			||||||
 | 
							rmsg.Event = config.EventTopicChange
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("Dropping message with unknown type: %s", ev.Type)
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
 | 
				
			||||||
 | 
						for message := range b.messageChan {
 | 
				
			||||||
 | 
							message := message
 | 
				
			||||||
 | 
							// skip messages with same ID, apparently messages get duplicated for an unknown reason
 | 
				
			||||||
 | 
							if _, ok := b.cache.Get(message.ID); ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.cache.Add(message.ID, true)
 | 
				
			||||||
 | 
							b.Log.Debugf("message %#v", message)
 | 
				
			||||||
 | 
							m := message
 | 
				
			||||||
 | 
							if b.skipMessage(&m) {
 | 
				
			||||||
 | 
								b.Log.Debugf("Skipped message: %#v", message)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rmsg := &config.Message{Text: message.Msg,
 | 
				
			||||||
 | 
								Username: message.User.UserName,
 | 
				
			||||||
 | 
								Channel:  b.getChannelName(message.RoomID),
 | 
				
			||||||
 | 
								Account:  b.Account,
 | 
				
			||||||
 | 
								UserID:   message.User.ID,
 | 
				
			||||||
 | 
								ID:       message.ID,
 | 
				
			||||||
 | 
								Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.handleAttachments(&message, rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handleStatusEvent returns false if the message should be dropped
 | 
				
			||||||
 | 
							// in that case it is probably some modification to the channel we do not want to relay
 | 
				
			||||||
 | 
							if b.handleStatusEvent(m, rmsg) {
 | 
				
			||||||
 | 
								messages <- rmsg
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) handleAttachments(message *models.Message, rmsg *config.Message) {
 | 
				
			||||||
 | 
						if rmsg.Text == "" {
 | 
				
			||||||
 | 
							for _, attachment := range message.Attachments {
 | 
				
			||||||
 | 
								if attachment.Title != "" {
 | 
				
			||||||
 | 
									rmsg.Text = attachment.Title + "\n"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if attachment.Title != "" && attachment.Text != "" {
 | 
				
			||||||
 | 
									rmsg.Text += "\n"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if attachment.Text != "" {
 | 
				
			||||||
 | 
									rmsg.Text += attachment.Text
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range message.Attachments {
 | 
				
			||||||
 | 
							if err := b.handleDownloadFile(rmsg, &message.Attachments[i]); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Could not download incoming file: %#v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) handleDownloadFile(rmsg *config.Message, file *models.Attachment) error {
 | 
				
			||||||
 | 
						downloadURL := b.GetString("server") + file.TitleLink
 | 
				
			||||||
 | 
						data, err := helper.DownloadFileAuthRocket(downloadURL, b.user.Token, b.user.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("download %s failed %#v", downloadURL, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, rmsg, file.Title, rmsg.Text, downloadURL, data, b.General)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							if err := b.uploadFile(&fi, b.getChannelID(msg.Channel)); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										201
									
								
								bridge/rocketchat/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								bridge/rocketchat/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,201 @@
 | 
				
			|||||||
 | 
					package brocketchat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/hook/rockethook"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterhook"
 | 
				
			||||||
 | 
						"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
 | 
				
			||||||
 | 
						"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
 | 
				
			||||||
 | 
						"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
 | 
				
			||||||
 | 
						"github.com/nelsonken/gomf"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) doConnectWebhookBind() error {
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case b.GetString("WebhookURL") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
 | 
				
			||||||
 | 
							b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
				
			||||||
 | 
								matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
				
			||||||
 | 
									DisableServer: true})
 | 
				
			||||||
 | 
							b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
 | 
				
			||||||
 | 
						case b.GetString("Login") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using login/password (sending)")
 | 
				
			||||||
 | 
							err := b.apiLogin()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using webhookbindaddress (receiving)")
 | 
				
			||||||
 | 
							b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) doConnectWebhookURL() error {
 | 
				
			||||||
 | 
						b.Log.Info("Connecting using webhookurl (sending)")
 | 
				
			||||||
 | 
						b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
				
			||||||
 | 
							matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
				
			||||||
 | 
								DisableServer: true})
 | 
				
			||||||
 | 
						if b.GetString("Login") != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using login/password (receiving)")
 | 
				
			||||||
 | 
							err := b.apiLogin()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) apiLogin() error {
 | 
				
			||||||
 | 
						b.Log.Debugf("handling apiLogin()")
 | 
				
			||||||
 | 
						credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")}
 | 
				
			||||||
 | 
						if b.GetString("Token") != "" {
 | 
				
			||||||
 | 
							credentials = &models.UserCredentials{ID: b.GetString("Login"), Token: b.GetString("Token")}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						myURL, err := url.Parse(b.GetString("server"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						client, err := realtime.NewClient(myURL, b.GetBool("debug"))
 | 
				
			||||||
 | 
						b.c = client
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						restclient := rest.NewClient(myURL, b.GetBool("debug"))
 | 
				
			||||||
 | 
						user, err := b.c.Login(credentials)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.user = user
 | 
				
			||||||
 | 
						b.r = restclient
 | 
				
			||||||
 | 
						err = b.r.Login(credentials)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) getChannelName(id string) string {
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						defer b.RUnlock()
 | 
				
			||||||
 | 
						if name, ok := b.channelMap[id]; ok {
 | 
				
			||||||
 | 
							return name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) getChannelID(name string) string {
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						defer b.RUnlock()
 | 
				
			||||||
 | 
						for k, v := range b.channelMap {
 | 
				
			||||||
 | 
							if v == name || v == "#"+name {
 | 
				
			||||||
 | 
								return k
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) skipMessage(message *models.Message) bool {
 | 
				
			||||||
 | 
						return message.User.ID == b.user.ID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
 | 
				
			||||||
 | 
						fb := gomf.New()
 | 
				
			||||||
 | 
						if err := fb.WriteField("description", fi.Comment); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sp := strings.Split(fi.Name, ".")
 | 
				
			||||||
 | 
						mtype := mime.TypeByExtension("." + sp[len(sp)-1])
 | 
				
			||||||
 | 
						if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := fb.WriteFile("file", fi.Name, mtype, *fi.Data); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req, err := fb.GetHTTPRequest(context.TODO(), b.GetString("server")+"/api/v1/rooms.upload/"+channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req.Header.Add("X-Auth-Token", b.user.Token)
 | 
				
			||||||
 | 
						req.Header.Add("X-User-Id", b.user.ID)
 | 
				
			||||||
 | 
						client := &http.Client{
 | 
				
			||||||
 | 
							Timeout: time.Second * 5,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resp, err := client.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						body, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if resp.StatusCode != 200 {
 | 
				
			||||||
 | 
							b.Log.Errorf("failed: %#v", string(body))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// sendWebhook uses the configured WebhookURL to send the message
 | 
				
			||||||
 | 
					func (b *Brocketchat) sendWebhook(msg *config.Message) error {
 | 
				
			||||||
 | 
						// skip events
 | 
				
			||||||
 | 
						if msg.Event != "" {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("PrefixMessagesWithNick") {
 | 
				
			||||||
 | 
							msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(msg, b.General) {
 | 
				
			||||||
 | 
								rmsg := rmsg // scopelint
 | 
				
			||||||
 | 
								iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
 | 
				
			||||||
 | 
								matterMessage := matterhook.OMessage{
 | 
				
			||||||
 | 
									IconURL:  iconURL,
 | 
				
			||||||
 | 
									Channel:  rmsg.Channel,
 | 
				
			||||||
 | 
									UserName: rmsg.Username,
 | 
				
			||||||
 | 
									Text:     rmsg.Text,
 | 
				
			||||||
 | 
									Props:    make(map[string]interface{}),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := b.mh.Send(matterMessage); err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("sendWebhook failed: %s ", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// webhook doesn't support file uploads, so we add the url manually
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
									fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
									if fi.URL != "" {
 | 
				
			||||||
 | 
										msg.Text += fi.URL
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						iconURL := config.GetIconURL(msg, b.GetString("iconurl"))
 | 
				
			||||||
 | 
						matterMessage := matterhook.OMessage{
 | 
				
			||||||
 | 
							IconURL:  iconURL,
 | 
				
			||||||
 | 
							Channel:  msg.Channel,
 | 
				
			||||||
 | 
							UserName: msg.Username,
 | 
				
			||||||
 | 
							Text:     msg.Text,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if msg.Avatar != "" {
 | 
				
			||||||
 | 
							matterMessage.IconURL = msg.Avatar
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err := b.mh.Send(matterMessage)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Info(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										181
									
								
								bridge/rocketchat/rocketchat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								bridge/rocketchat/rocketchat.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
				
			|||||||
 | 
					package brocketchat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/hook/rockethook"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterhook"
 | 
				
			||||||
 | 
						lru "github.com/hashicorp/golang-lru"
 | 
				
			||||||
 | 
						"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
 | 
				
			||||||
 | 
						"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
 | 
				
			||||||
 | 
						"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Brocketchat struct {
 | 
				
			||||||
 | 
						mh    *matterhook.Client
 | 
				
			||||||
 | 
						rh    *rockethook.Client
 | 
				
			||||||
 | 
						c     *realtime.Client
 | 
				
			||||||
 | 
						r     *rest.Client
 | 
				
			||||||
 | 
						cache *lru.Cache
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
						messageChan chan models.Message
 | 
				
			||||||
 | 
						channelMap  map[string]string
 | 
				
			||||||
 | 
						user        *models.User
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						sUserJoined       = "uj"
 | 
				
			||||||
 | 
						sUserLeft         = "ul"
 | 
				
			||||||
 | 
						sRoomChangedTopic = "room_changed_topic"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						newCache, err := lru.New(100)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							cfg.Log.Fatalf("Could not create LRU cache for rocketchat bridge: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b := &Brocketchat{
 | 
				
			||||||
 | 
							Config:      cfg,
 | 
				
			||||||
 | 
							messageChan: make(chan models.Message),
 | 
				
			||||||
 | 
							channelMap:  make(map[string]string),
 | 
				
			||||||
 | 
							cache:       newCache,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("enabling rocketchat")
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) Command(cmd string) string {
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) Connect() error {
 | 
				
			||||||
 | 
						if b.GetString("WebhookBindAddress") != "" {
 | 
				
			||||||
 | 
							if err := b.doConnectWebhookBind(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleRocket()
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case b.GetString("WebhookURL") != "":
 | 
				
			||||||
 | 
							if err := b.doConnectWebhookURL(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleRocket()
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						case b.GetString("Login") != "":
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using login/password (sending and receiving)")
 | 
				
			||||||
 | 
							err := b.apiLogin()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleRocket()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
 | 
				
			||||||
 | 
							b.GetString("Login") == "" {
 | 
				
			||||||
 | 
							return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server configured")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						if b.c == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						id, err := b.c.GetChannelId(strings.TrimPrefix(channel.Name, "#"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Lock()
 | 
				
			||||||
 | 
						b.channelMap[id] = channel.Name
 | 
				
			||||||
 | 
						b.Unlock()
 | 
				
			||||||
 | 
						mychannel := &models.Channel{ID: id, Name: strings.TrimPrefix(channel.Name, "#")}
 | 
				
			||||||
 | 
						if err := b.c.JoinChannel(id); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := b.c.SubscribeToMessageStream(mychannel, b.messageChan); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Brocketchat) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// strip the # if people has set this
 | 
				
			||||||
 | 
						msg.Channel = strings.TrimPrefix(msg.Channel, "#")
 | 
				
			||||||
 | 
						channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make a action /me of the message
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
							msg.Text = "_" + msg.Text + "_"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return msg.ID, b.c.DeleteMessage(&models.Message{ID: msg.ID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use webhook to send the message
 | 
				
			||||||
 | 
						if b.GetString("WebhookURL") != "" {
 | 
				
			||||||
 | 
							return "", b.sendWebhook(&msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Prepend nick if configured
 | 
				
			||||||
 | 
						if b.GetBool("PrefixMessagesWithNick") {
 | 
				
			||||||
 | 
							msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message if we have an ID
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							return msg.ID, b.c.EditMessage(&models.Message{ID: msg.ID, Msg: msg.Text, RoomID: b.getChannelID(msg.Channel)})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file if it exists
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								// strip the # if people has set this
 | 
				
			||||||
 | 
								rmsg.Channel = strings.TrimPrefix(rmsg.Channel, "#")
 | 
				
			||||||
 | 
								smsg := &models.Message{
 | 
				
			||||||
 | 
									RoomID: b.getChannelID(rmsg.Channel),
 | 
				
			||||||
 | 
									Msg:    rmsg.Username + rmsg.Text,
 | 
				
			||||||
 | 
									PostMessage: models.PostMessage{
 | 
				
			||||||
 | 
										Avatar: rmsg.Avatar,
 | 
				
			||||||
 | 
										Alias:  rmsg.Username,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if _, err := b.c.SendMessage(smsg); err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("SendMessage failed: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return "", b.handleUploadFile(&msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						smsg := &models.Message{
 | 
				
			||||||
 | 
							RoomID: channel.ID,
 | 
				
			||||||
 | 
							Msg:    msg.Text,
 | 
				
			||||||
 | 
							PostMessage: models.PostMessage{
 | 
				
			||||||
 | 
								Avatar: msg.Avatar,
 | 
				
			||||||
 | 
								Alias:  msg.Username,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg, err := b.c.SendMessage(smsg)
 | 
				
			||||||
 | 
						if rmsg == nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return rmsg.ID, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										415
									
								
								bridge/slack/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										415
									
								
								bridge/slack/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,415 @@
 | 
				
			|||||||
 | 
					package bslack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/slack-go/slack"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ErrEventIgnored is for events that should be ignored
 | 
				
			||||||
 | 
					var ErrEventIgnored = errors.New("this event message should ignored")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) handleSlack() {
 | 
				
			||||||
 | 
						messages := make(chan *config.Message)
 | 
				
			||||||
 | 
						if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Choosing webhooks based receiving")
 | 
				
			||||||
 | 
							go b.handleMatterHook(messages)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.Log.Debugf("Choosing token based receiving")
 | 
				
			||||||
 | 
							go b.handleSlackClient(messages)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						time.Sleep(time.Second)
 | 
				
			||||||
 | 
						b.Log.Debug("Start listening for Slack messages")
 | 
				
			||||||
 | 
						for message := range messages {
 | 
				
			||||||
 | 
							// don't do any action on deleted/typing messages
 | 
				
			||||||
 | 
							if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete &&
 | 
				
			||||||
 | 
								message.Event != config.EventFileDelete {
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
 | 
				
			||||||
 | 
								// cleanup the message
 | 
				
			||||||
 | 
								message.Text = b.replaceMention(message.Text)
 | 
				
			||||||
 | 
								message.Text = b.replaceVariable(message.Text)
 | 
				
			||||||
 | 
								message.Text = b.replaceChannel(message.Text)
 | 
				
			||||||
 | 
								message.Text = b.replaceURL(message.Text)
 | 
				
			||||||
 | 
								message.Text = b.replaceb0rkedMarkDown(message.Text)
 | 
				
			||||||
 | 
								message.Text = html.UnescapeString(message.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Add the avatar
 | 
				
			||||||
 | 
								message.Avatar = b.users.getAvatar(message.UserID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("<= Message is %#v", message)
 | 
				
			||||||
 | 
							b.Remote <- *message
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) handleSlackClient(messages chan *config.Message) {
 | 
				
			||||||
 | 
						for msg := range b.rtm.IncomingEvents {
 | 
				
			||||||
 | 
							if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport {
 | 
				
			||||||
 | 
								b.Log.Debugf("== Receiving event %#v", msg.Data)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							switch ev := msg.Data.(type) {
 | 
				
			||||||
 | 
							case *slack.UserTypingEvent:
 | 
				
			||||||
 | 
								if !b.GetBool("ShowUserTyping") {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								rmsg, err := b.handleTypingEvent(ev)
 | 
				
			||||||
 | 
								if err == ErrEventIgnored {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								} else if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("%#v", err)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								messages <- rmsg
 | 
				
			||||||
 | 
							case *slack.MessageEvent:
 | 
				
			||||||
 | 
								if b.skipMessageEvent(ev) {
 | 
				
			||||||
 | 
									b.Log.Debugf("Skipped message: %#v", ev)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								rmsg, err := b.handleMessageEvent(ev)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("%#v", err)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								messages <- rmsg
 | 
				
			||||||
 | 
							case *slack.FileDeletedEvent:
 | 
				
			||||||
 | 
								rmsg, err := b.handleFileDeletedEvent(ev)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("%#v", err)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								messages <- rmsg
 | 
				
			||||||
 | 
							case *slack.OutgoingErrorEvent:
 | 
				
			||||||
 | 
								b.Log.Debugf("%#v", ev.Error())
 | 
				
			||||||
 | 
							case *slack.ChannelJoinedEvent:
 | 
				
			||||||
 | 
								// When we join a channel we update the full list of users as
 | 
				
			||||||
 | 
								// well as the information for the channel that we joined as this
 | 
				
			||||||
 | 
								// should now tell that we are a member of it.
 | 
				
			||||||
 | 
								b.channels.registerChannel(ev.Channel)
 | 
				
			||||||
 | 
							case *slack.ConnectedEvent:
 | 
				
			||||||
 | 
								b.si = ev.Info
 | 
				
			||||||
 | 
								b.channels.populateChannels(true)
 | 
				
			||||||
 | 
								b.users.populateUsers(true)
 | 
				
			||||||
 | 
							case *slack.InvalidAuthEvent:
 | 
				
			||||||
 | 
								b.Log.Fatalf("Invalid Token %#v", ev)
 | 
				
			||||||
 | 
							case *slack.ConnectionErrorEvent:
 | 
				
			||||||
 | 
								b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
 | 
				
			||||||
 | 
							case *slack.MemberJoinedChannelEvent:
 | 
				
			||||||
 | 
								b.users.populateUser(ev.User)
 | 
				
			||||||
 | 
							case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent:
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							case *slack.UserChangeEvent:
 | 
				
			||||||
 | 
								b.users.invalidateUser(ev.User.ID)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								b.Log.Debugf("Unhandled incoming event: %T", ev)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) handleMatterHook(messages chan *config.Message) {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							message := b.mh.Receive()
 | 
				
			||||||
 | 
							b.Log.Debugf("receiving from matterhook (slack) %#v", message)
 | 
				
			||||||
 | 
							if message.UserName == "slackbot" {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							messages <- &config.Message{
 | 
				
			||||||
 | 
								Username: message.UserName,
 | 
				
			||||||
 | 
								Text:     message.Text,
 | 
				
			||||||
 | 
								Channel:  message.ChannelName,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// skipMessageEvent skips event that need to be skipped :-)
 | 
				
			||||||
 | 
					func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
 | 
				
			||||||
 | 
						switch ev.SubType {
 | 
				
			||||||
 | 
						case sChannelLeave, sChannelJoin:
 | 
				
			||||||
 | 
							return b.GetBool(noSendJoinConfig)
 | 
				
			||||||
 | 
						case sPinnedItem, sUnpinnedItem:
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						case sChannelTopic, sChannelPurpose:
 | 
				
			||||||
 | 
							// Skip the event if our bot/user account changed the topic/purpose
 | 
				
			||||||
 | 
							if ev.User == b.si.User.ID {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check for our callback ID
 | 
				
			||||||
 | 
						hasOurCallbackID := false
 | 
				
			||||||
 | 
						if len(ev.Blocks.BlockSet) == 1 {
 | 
				
			||||||
 | 
							block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock)
 | 
				
			||||||
 | 
							hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ev.SubMessage != nil {
 | 
				
			||||||
 | 
							// It seems ev.SubMessage.Edited == nil when slack unfurls.
 | 
				
			||||||
 | 
							// Do not forward these messages. See Github issue #266.
 | 
				
			||||||
 | 
							if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
 | 
				
			||||||
 | 
								ev.SubMessage.Edited == nil {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// see hidden subtypes at https://api.slack.com/events/message
 | 
				
			||||||
 | 
							// these messages are sent when we add a message to a thread #709
 | 
				
			||||||
 | 
							if ev.SubType == "message_replied" && ev.Hidden {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(ev.SubMessage.Blocks.BlockSet) == 1 {
 | 
				
			||||||
 | 
								block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock)
 | 
				
			||||||
 | 
								hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Skip any messages that we made ourselves or from 'slackbot' (see #527).
 | 
				
			||||||
 | 
						if ev.Username == sSlackBotUser ||
 | 
				
			||||||
 | 
							(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(ev.Files) > 0 {
 | 
				
			||||||
 | 
							return b.filesCached(ev.Files)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) filesCached(files []slack.File) bool {
 | 
				
			||||||
 | 
						for i := range files {
 | 
				
			||||||
 | 
							if !b.fileCached(&files[i]) {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleMessageEvent handles the message events. Together with any called sub-methods,
 | 
				
			||||||
 | 
					// this method implements the following event processing pipeline:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 1. Check if the message should be ignored.
 | 
				
			||||||
 | 
					//    NOTE: This is not actually part of the method below but is done just before it
 | 
				
			||||||
 | 
					//          is called via the 'skipMessageEvent()' method.
 | 
				
			||||||
 | 
					// 2. Populate the Matterbridge message that will be sent to the router based on the
 | 
				
			||||||
 | 
					//    received event and logic that is common to all events that are not skipped.
 | 
				
			||||||
 | 
					// 3. Detect and handle any message that is "status" related (think join channel, etc.).
 | 
				
			||||||
 | 
					//    This might result in an early exit from the pipeline and passing of the
 | 
				
			||||||
 | 
					//    pre-populated message to the Matterbridge router.
 | 
				
			||||||
 | 
					// 4. Handle the specific case of messages that edit existing messages depending on
 | 
				
			||||||
 | 
					//    configuration.
 | 
				
			||||||
 | 
					// 5. Handle any attachments of the received event.
 | 
				
			||||||
 | 
					// 6. Check that the Matterbridge message that we end up with after at the end of the
 | 
				
			||||||
 | 
					//    pipeline is valid before sending it to the Matterbridge router.
 | 
				
			||||||
 | 
					func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) {
 | 
				
			||||||
 | 
						rmsg, err := b.populateReceivedMessage(ev)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle some message types early.
 | 
				
			||||||
 | 
						if b.handleStatusEvent(ev, rmsg) {
 | 
				
			||||||
 | 
							return rmsg, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.handleAttachments(ev, rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify that we have the right information and the message
 | 
				
			||||||
 | 
						// is well-formed before sending it out to the router.
 | 
				
			||||||
 | 
						if len(ev.Files) == 0 && (rmsg.Text == "" || rmsg.Username == "") {
 | 
				
			||||||
 | 
							if ev.BotID != "" {
 | 
				
			||||||
 | 
								// This is probably a webhook we couldn't resolve.
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if ev.SubMessage != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.SubMessage)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return rmsg, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) {
 | 
				
			||||||
 | 
						if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok {
 | 
				
			||||||
 | 
							channel, err := b.channels.getChannelByID(rawChannel.(string))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return &config.Message{
 | 
				
			||||||
 | 
								Event:    config.EventFileDelete,
 | 
				
			||||||
 | 
								Text:     config.EventFileDelete,
 | 
				
			||||||
 | 
								Channel:  channel.Name,
 | 
				
			||||||
 | 
								Account:  b.Account,
 | 
				
			||||||
 | 
								ID:       ev.FileID,
 | 
				
			||||||
 | 
								Protocol: b.Protocol,
 | 
				
			||||||
 | 
							}, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
 | 
				
			||||||
 | 
						switch ev.SubType {
 | 
				
			||||||
 | 
						case sChannelJoined, sMemberJoined:
 | 
				
			||||||
 | 
							// There's no further processing needed on channel events
 | 
				
			||||||
 | 
							// so we return 'true'.
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						case sChannelJoin, sChannelLeave:
 | 
				
			||||||
 | 
							rmsg.Username = sSystemUser
 | 
				
			||||||
 | 
							rmsg.Event = config.EventJoinLeave
 | 
				
			||||||
 | 
						case sChannelTopic, sChannelPurpose:
 | 
				
			||||||
 | 
							b.channels.populateChannels(false)
 | 
				
			||||||
 | 
							rmsg.Event = config.EventTopicChange
 | 
				
			||||||
 | 
						case sMessageChanged:
 | 
				
			||||||
 | 
							rmsg.Text = ev.SubMessage.Text
 | 
				
			||||||
 | 
							// handle deleted thread starting messages
 | 
				
			||||||
 | 
							if ev.SubMessage.Text == "This message was deleted." {
 | 
				
			||||||
 | 
								rmsg.Event = config.EventMsgDelete
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case sMessageDeleted:
 | 
				
			||||||
 | 
							rmsg.Text = config.EventMsgDelete
 | 
				
			||||||
 | 
							rmsg.Event = config.EventMsgDelete
 | 
				
			||||||
 | 
							rmsg.ID = ev.DeletedTimestamp
 | 
				
			||||||
 | 
							// If a message is being deleted we do not need to process
 | 
				
			||||||
 | 
							// the event any further so we return 'true'.
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						case sMeMessage:
 | 
				
			||||||
 | 
							rmsg.Event = config.EventUserAction
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getMessageTitle(attach *slack.Attachment) string {
 | 
				
			||||||
 | 
						if attach.TitleLink != "" {
 | 
				
			||||||
 | 
							return fmt.Sprintf("[%s](%s)\n", attach.Title, attach.TitleLink)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return attach.Title
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
 | 
				
			||||||
 | 
						// File comments are set by the system (because there is no username given).
 | 
				
			||||||
 | 
						if ev.SubType == sFileComment {
 | 
				
			||||||
 | 
							rmsg.Username = sSystemUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// See if we have some text in the attachments.
 | 
				
			||||||
 | 
						if rmsg.Text == "" {
 | 
				
			||||||
 | 
							for i, attach := range ev.Attachments {
 | 
				
			||||||
 | 
								if attach.Text != "" {
 | 
				
			||||||
 | 
									if attach.Title != "" {
 | 
				
			||||||
 | 
										rmsg.Text = getMessageTitle(&ev.Attachments[i])
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									rmsg.Text += attach.Text
 | 
				
			||||||
 | 
									if attach.Footer != "" {
 | 
				
			||||||
 | 
										rmsg.Text += "\n\n" + attach.Footer
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									rmsg.Text = attach.Fallback
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Save the attachments, so that we can send them to other slack (compatible) bridges.
 | 
				
			||||||
 | 
						if len(ev.Attachments) > 0 {
 | 
				
			||||||
 | 
							rmsg.Extra[sSlackAttachment] = append(rmsg.Extra[sSlackAttachment], ev.Attachments)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
 | 
				
			||||||
 | 
						for i := range ev.Files {
 | 
				
			||||||
 | 
							// keep reference in cache on which channel we added this file
 | 
				
			||||||
 | 
							b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel)
 | 
				
			||||||
 | 
							if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Could not download incoming file: %#v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
 | 
				
			||||||
 | 
						if ev.User == b.si.User.ID {
 | 
				
			||||||
 | 
							return nil, ErrEventIgnored
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						channelInfo, err := b.channels.getChannelByID(ev.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &config.Message{
 | 
				
			||||||
 | 
							Channel: channelInfo.Name,
 | 
				
			||||||
 | 
							Account: b.Account,
 | 
				
			||||||
 | 
							Event:   config.EventUserTyping,
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadFile handles file download
 | 
				
			||||||
 | 
					func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error {
 | 
				
			||||||
 | 
						if b.fileCached(file) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Check that the file is neither too large nor blacklisted.
 | 
				
			||||||
 | 
						if err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General); err != nil {
 | 
				
			||||||
 | 
							b.Log.WithError(err).Infof("Skipping download of incoming file.")
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Actually download the file.
 | 
				
			||||||
 | 
						data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString(tokenConfig))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(*data) != file.Size && !retry {
 | 
				
			||||||
 | 
							b.Log.Debugf("Data size (%d) is not equal to size declared (%d)\n", len(*data), file.Size)
 | 
				
			||||||
 | 
							time.Sleep(1 * time.Second)
 | 
				
			||||||
 | 
							return b.handleDownloadFile(rmsg, file, true)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If a comment is attached to the file(s) it is in the 'Text' field of the Slack messge event
 | 
				
			||||||
 | 
						// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
 | 
				
			||||||
 | 
						// that the comment is not duplicated.
 | 
				
			||||||
 | 
						comment := rmsg.Text
 | 
				
			||||||
 | 
						rmsg.Text = ""
 | 
				
			||||||
 | 
						helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleGetChannelMembers handles messages containing the GetChannelMembers event
 | 
				
			||||||
 | 
					// Sends a message to the router containing *config.ChannelMembers
 | 
				
			||||||
 | 
					func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
 | 
				
			||||||
 | 
						if rmsg.Event != config.EventGetChannelMembers {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cMembers := b.channels.getChannelMembers(b.users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						extra := make(map[string][]interface{})
 | 
				
			||||||
 | 
						extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers)
 | 
				
			||||||
 | 
						msg := config.Message{
 | 
				
			||||||
 | 
							Extra:   extra,
 | 
				
			||||||
 | 
							Event:   config.EventGetChannelMembers,
 | 
				
			||||||
 | 
							Account: b.Account,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("sending msg to remote %#v", msg)
 | 
				
			||||||
 | 
						b.Remote <- msg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fileCached implements Matterbridge's caching logic for files
 | 
				
			||||||
 | 
					// shared via Slack.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// We consider that a file was cached if its ID was added in the last minute or
 | 
				
			||||||
 | 
					// it's name was registered in the last 10 seconds. This ensures that an
 | 
				
			||||||
 | 
					// identically named file but with different content will be uploaded correctly
 | 
				
			||||||
 | 
					// (the assumption is that such name collisions will not occur within the given
 | 
				
			||||||
 | 
					// timeframes).
 | 
				
			||||||
 | 
					func (b *Bslack) fileCached(file *slack.File) bool {
 | 
				
			||||||
 | 
						if ts, ok := b.cache.Get("file" + file.ID); ok && time.Since(ts.(time.Time)) < time.Minute {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						} else if ts, ok = b.cache.Get("filename" + file.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										255
									
								
								bridge/slack/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								bridge/slack/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,255 @@
 | 
				
			|||||||
 | 
					package bslack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"github.com/slack-go/slack"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
 | 
				
			||||||
 | 
					// router before we apply message-dependent modifications.
 | 
				
			||||||
 | 
					func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
 | 
				
			||||||
 | 
						// Use our own func because rtm.GetChannelInfo doesn't work for private channels.
 | 
				
			||||||
 | 
						channel, err := b.channels.getChannelByID(ev.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := &config.Message{
 | 
				
			||||||
 | 
							Text:     ev.Text,
 | 
				
			||||||
 | 
							Channel:  channel.Name,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							ID:       ev.Timestamp,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ParentID: ev.ThreadTimestamp,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.useChannelID {
 | 
				
			||||||
 | 
							rmsg.Channel = "ID:" + channel.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle 'edit' messages.
 | 
				
			||||||
 | 
						if ev.SubMessage != nil && !b.GetBool(editDisableConfig) {
 | 
				
			||||||
 | 
							rmsg.ID = ev.SubMessage.Timestamp
 | 
				
			||||||
 | 
							if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
 | 
				
			||||||
 | 
								b.Log.Debugf("SubMessage %#v", ev.SubMessage)
 | 
				
			||||||
 | 
								rmsg.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// For edits, only submessage has thread ts.
 | 
				
			||||||
 | 
						// Ensures edits to threaded messages maintain their prefix hint on the
 | 
				
			||||||
 | 
						// unthreaded end.
 | 
				
			||||||
 | 
						if ev.SubMessage != nil {
 | 
				
			||||||
 | 
							rmsg.ParentID = ev.SubMessage.ThreadTimestamp
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return rmsg, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
 | 
				
			||||||
 | 
						if ev.SubType == sMessageDeleted || ev.SubType == sFileComment {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// First, deal with bot-originating messages but only do so when not using webhooks: we
 | 
				
			||||||
 | 
						// would not be able to distinguish which bot would be sending them.
 | 
				
			||||||
 | 
						if err := b.populateMessageWithBotInfo(ev, rmsg); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Second, deal with "real" users if we have the necessary information.
 | 
				
			||||||
 | 
						var userID string
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case ev.User != "":
 | 
				
			||||||
 | 
							userID = ev.User
 | 
				
			||||||
 | 
						case ev.SubMessage != nil && ev.SubMessage.User != "":
 | 
				
			||||||
 | 
							userID = ev.SubMessage.User
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := b.users.getUser(userID)
 | 
				
			||||||
 | 
						if user == nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("could not find information for user with id %s", ev.User)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg.UserID = user.ID
 | 
				
			||||||
 | 
						rmsg.Username = user.Name
 | 
				
			||||||
 | 
						if user.Profile.DisplayName != "" {
 | 
				
			||||||
 | 
							rmsg.Username = user.Profile.DisplayName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetBool("UseFullName") && user.Profile.RealName != "" {
 | 
				
			||||||
 | 
							rmsg.Username = user.Profile.RealName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
 | 
				
			||||||
 | 
						if ev.BotID == "" || b.GetString(outgoingWebhookConfig) != "" {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						var bot *slack.Bot
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							bot, err = b.rtm.GetBotInfo(ev.BotID)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = handleRateLimit(b.Log, err); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Could not retrieve bot information: %#v", err)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("Found bot %#v", bot)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if bot.Name != "" {
 | 
				
			||||||
 | 
							rmsg.Username = bot.Name
 | 
				
			||||||
 | 
							if ev.Username != "" {
 | 
				
			||||||
 | 
								rmsg.Username = ev.Username
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							rmsg.UserID = bot.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						mentionRE        = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
 | 
				
			||||||
 | 
						channelRE        = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
 | 
				
			||||||
 | 
						variableRE       = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
 | 
				
			||||||
 | 
						urlRE            = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
 | 
				
			||||||
 | 
						codeFenceRE      = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
 | 
				
			||||||
 | 
						topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
 | 
				
			||||||
 | 
						r := topicOrPurposeRE.FindStringSubmatch(text)
 | 
				
			||||||
 | 
						if len(r) == 5 {
 | 
				
			||||||
 | 
							action, updateType, extracted := r[2], r[3], r[4]
 | 
				
			||||||
 | 
							switch action {
 | 
				
			||||||
 | 
							case "set":
 | 
				
			||||||
 | 
								return updateType, extracted
 | 
				
			||||||
 | 
							case "cleared":
 | 
				
			||||||
 | 
								return updateType, ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Warnf("Encountered channel topic or purpose change message with unexpected format: %s", text)
 | 
				
			||||||
 | 
						return "unknown", ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
 | 
				
			||||||
 | 
					func (b *Bslack) replaceMention(text string) string {
 | 
				
			||||||
 | 
						replaceFunc := func(match string) string {
 | 
				
			||||||
 | 
							userID := strings.Trim(match, "@<>")
 | 
				
			||||||
 | 
							if username := b.users.getUsername(userID); userID != "" {
 | 
				
			||||||
 | 
								return "@" + username
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return match
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return mentionRE.ReplaceAllStringFunc(text, replaceFunc)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
 | 
				
			||||||
 | 
					func (b *Bslack) replaceChannel(text string) string {
 | 
				
			||||||
 | 
						for _, r := range channelRE.FindAllStringSubmatch(text, -1) {
 | 
				
			||||||
 | 
							text = strings.Replace(text, r[0], "#"+r[1], 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return text
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @see https://api.slack.com/docs/message-formatting#variables
 | 
				
			||||||
 | 
					func (b *Bslack) replaceVariable(text string) string {
 | 
				
			||||||
 | 
						for _, r := range variableRE.FindAllStringSubmatch(text, -1) {
 | 
				
			||||||
 | 
							if r[2] != "" {
 | 
				
			||||||
 | 
								text = strings.Replace(text, r[0], "@"+r[2], 1)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								text = strings.Replace(text, r[0], "@"+r[1], 1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return text
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @see https://api.slack.com/docs/message-formatting#linking_to_urls
 | 
				
			||||||
 | 
					func (b *Bslack) replaceURL(text string) string {
 | 
				
			||||||
 | 
						return urlRE.ReplaceAllString(text, "[${2}](${1})")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) replaceb0rkedMarkDown(text string) string {
 | 
				
			||||||
 | 
						// taken from https://github.com/mattermost/mattermost-server/blob/master/app/slackimport.go
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						regexReplaceAllString := []struct {
 | 
				
			||||||
 | 
							regex *regexp.Regexp
 | 
				
			||||||
 | 
							rpl   string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							// bold
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`),
 | 
				
			||||||
 | 
								"$1**$2**",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							// strikethrough
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`),
 | 
				
			||||||
 | 
								"$1~~$2~~",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							// single paragraph blockquote
 | 
				
			||||||
 | 
							// Slack converts > character to >
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								regexp.MustCompile(`(?sm)^>`),
 | 
				
			||||||
 | 
								">",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, rule := range regexReplaceAllString {
 | 
				
			||||||
 | 
							text = rule.regex.ReplaceAllString(text, rule.rpl)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return text
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) replaceCodeFence(text string) string {
 | 
				
			||||||
 | 
						return codeFenceRE.ReplaceAllString(text, "```")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getUsersInConversation returns an array of userIDs that are members of channelID
 | 
				
			||||||
 | 
					func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
 | 
				
			||||||
 | 
						channelMembers := []string{}
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							queryParams := &slack.GetUsersInConversationParameters{
 | 
				
			||||||
 | 
								ChannelID: channelID,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							members, nextCursor, err := b.sc.GetUsersInConversation(queryParams)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if err = handleRateLimit(b.Log, err); err != nil {
 | 
				
			||||||
 | 
									return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							channelMembers = append(channelMembers, members...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if nextCursor == "" {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							queryParams.Cursor = nextCursor
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return channelMembers, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handleRateLimit(log *logrus.Entry, err error) error {
 | 
				
			||||||
 | 
						rateLimit, ok := err.(*slack.RateLimitedError)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
 | 
				
			||||||
 | 
						time.Sleep(rateLimit.RetryAfter)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								bridge/slack/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								bridge/slack/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					package bslack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestExtractTopicOrPurpose(t *testing.T) {
 | 
				
			||||||
 | 
						testcases := map[string]struct {
 | 
				
			||||||
 | 
							input          string
 | 
				
			||||||
 | 
							wantChangeType string
 | 
				
			||||||
 | 
							wantOutput     string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							"success - topic type":   {"@someone set channel topic: foo bar", "topic", "foo bar"},
 | 
				
			||||||
 | 
							"success - purpose type": {"@someone set channel purpose: foo bar", "purpose", "foo bar"},
 | 
				
			||||||
 | 
							"success - one line":     {"@someone set channel topic: foo bar", "topic", "foo bar"},
 | 
				
			||||||
 | 
							"success - multi-line":   {"@someone set channel topic: foo\nbar", "topic", "foo\nbar"},
 | 
				
			||||||
 | 
							"success - cleared":      {"@someone cleared channel topic", "topic", ""},
 | 
				
			||||||
 | 
							"error - unhandled":      {"some unmatched message", "unknown", ""},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger := logrus.New()
 | 
				
			||||||
 | 
						logger.SetOutput(ioutil.Discard)
 | 
				
			||||||
 | 
						cfg := &bridge.Config{Bridge: &bridge.Bridge{Log: logrus.NewEntry(logger)}}
 | 
				
			||||||
 | 
						b := newBridge(cfg)
 | 
				
			||||||
 | 
						for name, tc := range testcases {
 | 
				
			||||||
 | 
							gotChangeType, gotOutput := b.extractTopicOrPurpose(tc.input)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equalf(t, tc.wantChangeType, gotChangeType, "This testcase failed: %s", name)
 | 
				
			||||||
 | 
							assert.Equalf(t, tc.wantOutput, gotOutput, "This testcase failed: %s", name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										80
									
								
								bridge/slack/legacy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								bridge/slack/legacy.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					package bslack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterhook"
 | 
				
			||||||
 | 
						"github.com/slack-go/slack"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BLegacy struct {
 | 
				
			||||||
 | 
						*Bslack
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewLegacy(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &BLegacy{Bslack: newBridge(cfg)}
 | 
				
			||||||
 | 
						b.legacy = true
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *BLegacy) Connect() error {
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						defer b.RUnlock()
 | 
				
			||||||
 | 
						if b.GetString(incomingWebhookConfig) != "" {
 | 
				
			||||||
 | 
							switch {
 | 
				
			||||||
 | 
							case b.GetString(outgoingWebhookConfig) != "":
 | 
				
			||||||
 | 
								b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
 | 
				
			||||||
 | 
								b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
 | 
				
			||||||
 | 
									InsecureSkipVerify: b.GetBool(skipTLSConfig),
 | 
				
			||||||
 | 
									BindAddress:        b.GetString(incomingWebhookConfig),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							case b.GetString(tokenConfig) != "":
 | 
				
			||||||
 | 
								b.Log.Info("Connecting using token (sending)")
 | 
				
			||||||
 | 
								b.sc = slack.New(b.GetString(tokenConfig))
 | 
				
			||||||
 | 
								b.rtm = b.sc.NewRTM()
 | 
				
			||||||
 | 
								go b.rtm.ManageConnection()
 | 
				
			||||||
 | 
								b.Log.Info("Connecting using webhookbindaddress (receiving)")
 | 
				
			||||||
 | 
								b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
 | 
				
			||||||
 | 
									InsecureSkipVerify: b.GetBool(skipTLSConfig),
 | 
				
			||||||
 | 
									BindAddress:        b.GetString(incomingWebhookConfig),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								b.Log.Info("Connecting using webhookbindaddress (receiving)")
 | 
				
			||||||
 | 
								b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
 | 
				
			||||||
 | 
									InsecureSkipVerify: b.GetBool(skipTLSConfig),
 | 
				
			||||||
 | 
									BindAddress:        b.GetString(incomingWebhookConfig),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go b.handleSlack()
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString(outgoingWebhookConfig) != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using webhookurl (sending)")
 | 
				
			||||||
 | 
							b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
 | 
				
			||||||
 | 
								InsecureSkipVerify: b.GetBool(skipTLSConfig),
 | 
				
			||||||
 | 
								DisableServer:      true,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if b.GetString(tokenConfig) != "" {
 | 
				
			||||||
 | 
								b.Log.Info("Connecting using token (receiving)")
 | 
				
			||||||
 | 
								b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
 | 
				
			||||||
 | 
								b.channels = newChannelManager(b.Log, b.sc)
 | 
				
			||||||
 | 
								b.users = newUserManager(b.Log, b.sc)
 | 
				
			||||||
 | 
								b.rtm = b.sc.NewRTM()
 | 
				
			||||||
 | 
								go b.rtm.ManageConnection()
 | 
				
			||||||
 | 
								go b.handleSlack()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if b.GetString(tokenConfig) != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using token (sending and receiving)")
 | 
				
			||||||
 | 
							b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
 | 
				
			||||||
 | 
							b.channels = newChannelManager(b.Log, b.sc)
 | 
				
			||||||
 | 
							b.users = newUserManager(b.Log, b.sc)
 | 
				
			||||||
 | 
							b.rtm = b.sc.NewRTM()
 | 
				
			||||||
 | 
							go b.rtm.ManageConnection()
 | 
				
			||||||
 | 
							go b.handleSlack()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" {
 | 
				
			||||||
 | 
							return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										566
									
								
								bridge/slack/slack.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										566
									
								
								bridge/slack/slack.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,566 @@
 | 
				
			|||||||
 | 
					package bslack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/matterhook"
 | 
				
			||||||
 | 
						lru "github.com/hashicorp/golang-lru"
 | 
				
			||||||
 | 
						"github.com/rs/xid"
 | 
				
			||||||
 | 
						"github.com/slack-go/slack"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bslack struct {
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mh  *matterhook.Client
 | 
				
			||||||
 | 
						sc  *slack.Client
 | 
				
			||||||
 | 
						rtm *slack.RTM
 | 
				
			||||||
 | 
						si  *slack.Info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cache        *lru.Cache
 | 
				
			||||||
 | 
						uuid         string
 | 
				
			||||||
 | 
						useChannelID bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channels *channels
 | 
				
			||||||
 | 
						users    *users
 | 
				
			||||||
 | 
						legacy   bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						sHello               = "hello"
 | 
				
			||||||
 | 
						sChannelJoin         = "channel_join"
 | 
				
			||||||
 | 
						sChannelLeave        = "channel_leave"
 | 
				
			||||||
 | 
						sChannelJoined       = "channel_joined"
 | 
				
			||||||
 | 
						sMemberJoined        = "member_joined_channel"
 | 
				
			||||||
 | 
						sMessageChanged      = "message_changed"
 | 
				
			||||||
 | 
						sMessageDeleted      = "message_deleted"
 | 
				
			||||||
 | 
						sSlackAttachment     = "slack_attachment"
 | 
				
			||||||
 | 
						sPinnedItem          = "pinned_item"
 | 
				
			||||||
 | 
						sUnpinnedItem        = "unpinned_item"
 | 
				
			||||||
 | 
						sChannelTopic        = "channel_topic"
 | 
				
			||||||
 | 
						sChannelPurpose      = "channel_purpose"
 | 
				
			||||||
 | 
						sFileComment         = "file_comment"
 | 
				
			||||||
 | 
						sMeMessage           = "me_message"
 | 
				
			||||||
 | 
						sUserTyping          = "user_typing"
 | 
				
			||||||
 | 
						sLatencyReport       = "latency_report"
 | 
				
			||||||
 | 
						sSystemUser          = "system"
 | 
				
			||||||
 | 
						sSlackBotUser        = "slackbot"
 | 
				
			||||||
 | 
						cfileDownloadChannel = "file_download_channel"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tokenConfig           = "Token"
 | 
				
			||||||
 | 
						incomingWebhookConfig = "WebhookBindAddress"
 | 
				
			||||||
 | 
						outgoingWebhookConfig = "WebhookURL"
 | 
				
			||||||
 | 
						skipTLSConfig         = "SkipTLSVerify"
 | 
				
			||||||
 | 
						useNickPrefixConfig   = "PrefixMessagesWithNick"
 | 
				
			||||||
 | 
						editDisableConfig     = "EditDisable"
 | 
				
			||||||
 | 
						editSuffixConfig      = "EditSuffix"
 | 
				
			||||||
 | 
						iconURLConfig         = "iconurl"
 | 
				
			||||||
 | 
						noSendJoinConfig      = "nosendjoinpart"
 | 
				
			||||||
 | 
						messageLength         = 3000
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						// Print a deprecation warning for legacy non-bot tokens (#527).
 | 
				
			||||||
 | 
						token := cfg.GetString(tokenConfig)
 | 
				
			||||||
 | 
						if token != "" && !strings.HasPrefix(token, "xoxb") {
 | 
				
			||||||
 | 
							cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.")
 | 
				
			||||||
 | 
							cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.")
 | 
				
			||||||
 | 
							cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup")
 | 
				
			||||||
 | 
							return NewLegacy(cfg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return newBridge(cfg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newBridge(cfg *bridge.Config) *Bslack {
 | 
				
			||||||
 | 
						newCache, err := lru.New(5000)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b := &Bslack{
 | 
				
			||||||
 | 
							Config: cfg,
 | 
				
			||||||
 | 
							uuid:   xid.New().String(),
 | 
				
			||||||
 | 
							cache:  newCache,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) Command(cmd string) string {
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) Connect() error {
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						defer b.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" {
 | 
				
			||||||
 | 
							return errors.New("no connection method found: WebhookBindAddress, WebhookURL or Token need to be configured")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we have a token we use the Slack websocket-based RTM for both sending and receiving.
 | 
				
			||||||
 | 
						if token := b.GetString(tokenConfig); token != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Connecting using token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.channels = newChannelManager(b.Log, b.sc)
 | 
				
			||||||
 | 
							b.users = newUserManager(b.Log, b.sc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.rtm = b.sc.NewRTM()
 | 
				
			||||||
 | 
							go b.rtm.ManageConnection()
 | 
				
			||||||
 | 
							go b.handleSlack()
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// In absence of a token we fall back to incoming and outgoing Webhooks.
 | 
				
			||||||
 | 
						b.mh = matterhook.New(
 | 
				
			||||||
 | 
							"",
 | 
				
			||||||
 | 
							matterhook.Config{
 | 
				
			||||||
 | 
								InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
				
			||||||
 | 
								DisableServer:      true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if b.GetString(outgoingWebhookConfig) != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Using specified webhook for outgoing messages.")
 | 
				
			||||||
 | 
							b.mh.Url = b.GetString(outgoingWebhookConfig)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString(incomingWebhookConfig) != "" {
 | 
				
			||||||
 | 
							b.Log.Info("Setting up local webhook for incoming messages.")
 | 
				
			||||||
 | 
							b.mh.BindAddress = b.GetString(incomingWebhookConfig)
 | 
				
			||||||
 | 
							b.mh.DisableServer = false
 | 
				
			||||||
 | 
							go b.handleSlack()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) Disconnect() error {
 | 
				
			||||||
 | 
						return b.rtm.Disconnect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// JoinChannel only acts as a verification method that checks whether Matterbridge's
 | 
				
			||||||
 | 
					// Slack integration is already member of the channel. This is because Slack does not
 | 
				
			||||||
 | 
					// allow apps or bots to join channels themselves and they need to be invited
 | 
				
			||||||
 | 
					// manually by a user.
 | 
				
			||||||
 | 
					func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						// We can only join a channel through the Slack API.
 | 
				
			||||||
 | 
						if b.sc == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// try to join a channel when in legacy
 | 
				
			||||||
 | 
						if b.legacy {
 | 
				
			||||||
 | 
							_, _, _, err := b.sc.JoinConversation(channel.Name)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								switch err.Error() {
 | 
				
			||||||
 | 
								case "name_taken", "restricted_action":
 | 
				
			||||||
 | 
								case "default":
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.channels.populateChannels(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channelInfo, err := b.channels.getChannel(channel.Name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("could not join channel: %#v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if strings.HasPrefix(channel.Name, "ID:") {
 | 
				
			||||||
 | 
							b.useChannelID = true
 | 
				
			||||||
 | 
							channel.Name = channelInfo.Name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we can't join a channel unless we are using legacy tokens #651
 | 
				
			||||||
 | 
						if !channelInfo.IsMember && !b.legacy {
 | 
				
			||||||
 | 
							return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) Reload(cfg *bridge.Config) (string, error) {
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// Too noisy to log like other events
 | 
				
			||||||
 | 
						if msg.Event != config.EventUserTyping {
 | 
				
			||||||
 | 
							b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped"))
 | 
				
			||||||
 | 
						msg.Text = b.replaceCodeFence(msg.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make a action /me of the message
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
							msg.Text = "_" + msg.Text + "_"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use webhook to send the message
 | 
				
			||||||
 | 
						if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
 | 
				
			||||||
 | 
							return "", b.sendWebhook(msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return b.sendRTM(msg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// sendWebhook uses the configured WebhookURL to send the message
 | 
				
			||||||
 | 
					func (b *Bslack) sendWebhook(msg config.Message) error {
 | 
				
			||||||
 | 
						// Skip events.
 | 
				
			||||||
 | 
						if msg.Event != "" {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool(useNickPrefixConfig) {
 | 
				
			||||||
 | 
							msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							// This sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE.
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								rmsg := rmsg // scopelint
 | 
				
			||||||
 | 
								iconURL := config.GetIconURL(&rmsg, b.GetString(iconURLConfig))
 | 
				
			||||||
 | 
								matterMessage := matterhook.OMessage{
 | 
				
			||||||
 | 
									IconURL:  iconURL,
 | 
				
			||||||
 | 
									Channel:  msg.Channel,
 | 
				
			||||||
 | 
									UserName: rmsg.Username,
 | 
				
			||||||
 | 
									Text:     rmsg.Text,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := b.mh.Send(matterMessage); err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("Failed to send message: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Webhook doesn't support file uploads, so we add the URL manually.
 | 
				
			||||||
 | 
							for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
								fi, ok := f.(config.FileInfo)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									b.Log.Errorf("Received a file with unexpected content: %#v", f)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if fi.URL != "" {
 | 
				
			||||||
 | 
									msg.Text += " " + fi.URL
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we have native slack_attachments add them.
 | 
				
			||||||
 | 
						var attachs []slack.Attachment
 | 
				
			||||||
 | 
						for _, attach := range msg.Extra[sSlackAttachment] {
 | 
				
			||||||
 | 
							attachs = append(attachs, attach.([]slack.Attachment)...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						iconURL := config.GetIconURL(&msg, b.GetString(iconURLConfig))
 | 
				
			||||||
 | 
						matterMessage := matterhook.OMessage{
 | 
				
			||||||
 | 
							IconURL:     iconURL,
 | 
				
			||||||
 | 
							Attachments: attachs,
 | 
				
			||||||
 | 
							Channel:     msg.Channel,
 | 
				
			||||||
 | 
							UserName:    msg.Username,
 | 
				
			||||||
 | 
							Text:        msg.Text,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if msg.Avatar != "" {
 | 
				
			||||||
 | 
							matterMessage.IconURL = msg.Avatar
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := b.mh.Send(matterMessage); err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Failed to send message via webhook: %#v", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) sendRTM(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// Handle channelmember messages.
 | 
				
			||||||
 | 
						if handled := b.handleGetChannelMembers(&msg); handled {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channelInfo, err := b.channels.getChannel(msg.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("could not send message: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserTyping {
 | 
				
			||||||
 | 
							if b.GetBool("ShowUserTyping") {
 | 
				
			||||||
 | 
								b.rtm.SendMessage(b.rtm.NewTypingMessage(channelInfo.ID))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var handled bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle topic/purpose updates.
 | 
				
			||||||
 | 
						if handled, err = b.handleTopicOrPurpose(&msg, channelInfo); handled {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle prefix hint for unthreaded messages.
 | 
				
			||||||
 | 
						if msg.ParentNotFound() {
 | 
				
			||||||
 | 
							msg.ParentID = ""
 | 
				
			||||||
 | 
							msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle message deletions.
 | 
				
			||||||
 | 
						if handled, err = b.deleteMessage(&msg, channelInfo); handled {
 | 
				
			||||||
 | 
							return msg.ID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Prepend nickname if configured.
 | 
				
			||||||
 | 
						if b.GetBool(useNickPrefixConfig) {
 | 
				
			||||||
 | 
							msg.Text = msg.Username + msg.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle message edits.
 | 
				
			||||||
 | 
						if handled, err = b.editMessage(&msg, channelInfo); handled {
 | 
				
			||||||
 | 
							return msg.ID, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file if it exists.
 | 
				
			||||||
 | 
						if len(msg.Extra) > 0 {
 | 
				
			||||||
 | 
							extraMsgs := helper.HandleExtra(&msg, b.General)
 | 
				
			||||||
 | 
							for i := range extraMsgs {
 | 
				
			||||||
 | 
								rmsg := &extraMsgs[i]
 | 
				
			||||||
 | 
								rmsg.Text = rmsg.Username + rmsg.Text
 | 
				
			||||||
 | 
								_, err = b.postMessage(rmsg, channelInfo)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Error(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Upload files if necessary (from Slack, Telegram or Mattermost).
 | 
				
			||||||
 | 
							return b.uploadFile(&msg, channelInfo.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post message.
 | 
				
			||||||
 | 
						return b.postMessage(&msg, channelInfo)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) error {
 | 
				
			||||||
 | 
						var updateFunc func(channelID string, value string) (*slack.Channel, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						incomingChangeType, text := b.extractTopicOrPurpose(msg.Text)
 | 
				
			||||||
 | 
						switch incomingChangeType {
 | 
				
			||||||
 | 
						case "topic":
 | 
				
			||||||
 | 
							updateFunc = b.rtm.SetTopicOfConversation
 | 
				
			||||||
 | 
						case "purpose":
 | 
				
			||||||
 | 
							updateFunc = b.rtm.SetPurposeOfConversation
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType)
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							_, err := updateFunc(channelInfo.ID, text)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err = handleRateLimit(b.Log, err); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handles updating topic/purpose and determining whether to further propagate update messages.
 | 
				
			||||||
 | 
					func (b *Bslack) handleTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
 | 
				
			||||||
 | 
						if msg.Event != config.EventTopicChange {
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetBool("SyncTopic") {
 | 
				
			||||||
 | 
							return true, b.updateTopicOrPurpose(msg, channelInfo)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Pass along to normal message handlers.
 | 
				
			||||||
 | 
						if b.GetBool("ShowTopicChange") {
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Swallow message as handled no-op.
 | 
				
			||||||
 | 
						return true, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
 | 
				
			||||||
 | 
						if msg.Event != config.EventMsgDelete {
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Some protocols echo deletes, but with an empty ID.
 | 
				
			||||||
 | 
						if msg.ID == "" {
 | 
				
			||||||
 | 
							return true, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							_, _, err := b.rtm.DeleteMessage(channelInfo.ID, msg.ID)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return true, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = handleRateLimit(b.Log, err); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Failed to delete user message from Slack: %#v", err)
 | 
				
			||||||
 | 
								return true, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
 | 
				
			||||||
 | 
						if msg.ID == "" {
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						messageOptions := b.prepareMessageOptions(msg)
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return true, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = handleRateLimit(b.Log, err); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Failed to edit user message on Slack: %#v", err)
 | 
				
			||||||
 | 
								return true, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (string, error) {
 | 
				
			||||||
 | 
						// don't post empty messages
 | 
				
			||||||
 | 
						if msg.Text == "" {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						messageOptions := b.prepareMessageOptions(msg)
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return id, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = handleRateLimit(b.Log, err); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Failed to sent user message to Slack: %#v", err)
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// uploadFile handles native upload of files
 | 
				
			||||||
 | 
					func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, error) {
 | 
				
			||||||
 | 
						var messageID string
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi, ok := f.(config.FileInfo)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								b.Log.Errorf("Received a file with unexpected content: %#v", f)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if msg.Text == fi.Comment {
 | 
				
			||||||
 | 
								msg.Text = ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Because the result of the UploadFile is slower than the MessageEvent from slack
 | 
				
			||||||
 | 
							// we can't match on the file ID yet, so we have to match on the filename too.
 | 
				
			||||||
 | 
							ts := time.Now()
 | 
				
			||||||
 | 
							b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String())
 | 
				
			||||||
 | 
							b.cache.Add("filename"+fi.Name, ts)
 | 
				
			||||||
 | 
							initialComment := fmt.Sprintf("File from %s", msg.Username)
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								initialComment += fmt.Sprintf(" with comment: %s", fi.Comment)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							res, err := b.sc.UploadFile(slack.FileUploadParameters{
 | 
				
			||||||
 | 
								Reader:          bytes.NewReader(*fi.Data),
 | 
				
			||||||
 | 
								Filename:        fi.Name,
 | 
				
			||||||
 | 
								Channels:        []string{channelID},
 | 
				
			||||||
 | 
								InitialComment:  initialComment,
 | 
				
			||||||
 | 
								ThreadTimestamp: msg.ParentID,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("uploadfile %#v", err)
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if res.ID != "" {
 | 
				
			||||||
 | 
								b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
 | 
				
			||||||
 | 
								b.cache.Add("file"+res.ID, ts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// search for message id by uploaded file in private/public channels, get thread timestamp from uploaded file
 | 
				
			||||||
 | 
								if v, ok := res.Shares.Private[channelID]; ok && len(v) > 0 {
 | 
				
			||||||
 | 
									messageID = v[0].Ts
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if v, ok := res.Shares.Public[channelID]; ok && len(v) > 0 {
 | 
				
			||||||
 | 
									messageID = v[0].Ts
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return messageID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
 | 
				
			||||||
 | 
						params := slack.NewPostMessageParameters()
 | 
				
			||||||
 | 
						if b.GetBool(useNickPrefixConfig) {
 | 
				
			||||||
 | 
							params.AsUser = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						params.Username = msg.Username
 | 
				
			||||||
 | 
						params.LinkNames = 1 // replace mentions
 | 
				
			||||||
 | 
						params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig))
 | 
				
			||||||
 | 
						params.ThreadTimestamp = msg.ParentID
 | 
				
			||||||
 | 
						if msg.Avatar != "" {
 | 
				
			||||||
 | 
							params.IconURL = msg.Avatar
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var attachments []slack.Attachment
 | 
				
			||||||
 | 
						// add file attachments
 | 
				
			||||||
 | 
						attachments = append(attachments, b.createAttach(msg.Extra)...)
 | 
				
			||||||
 | 
						// add slack attachments (from another slack bridge)
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, attach := range msg.Extra[sSlackAttachment] {
 | 
				
			||||||
 | 
								attachments = append(attachments, attach.([]slack.Attachment)...)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var opts []slack.MsgOption
 | 
				
			||||||
 | 
						opts = append(opts,
 | 
				
			||||||
 | 
							// provide regular text field (fallback used in Slack notifications, etc.)
 | 
				
			||||||
 | 
							slack.MsgOptionText(msg.Text, false),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// add a callback ID so we can see we created it
 | 
				
			||||||
 | 
							slack.MsgOptionBlocks(slack.NewSectionBlock(
 | 
				
			||||||
 | 
								slack.NewTextBlockObject(slack.MarkdownType, msg.Text, false, false),
 | 
				
			||||||
 | 
								nil, nil,
 | 
				
			||||||
 | 
								slack.SectionBlockOptionBlockID("matterbridge_"+b.uuid),
 | 
				
			||||||
 | 
							)),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							slack.MsgOptionEnableLinkUnfurl(),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						opts = append(opts, slack.MsgOptionAttachments(attachments...))
 | 
				
			||||||
 | 
						opts = append(opts, slack.MsgOptionPostMessageParameters(params))
 | 
				
			||||||
 | 
						return opts
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment {
 | 
				
			||||||
 | 
						var attachements []slack.Attachment
 | 
				
			||||||
 | 
						for _, v := range extra["attachments"] {
 | 
				
			||||||
 | 
							entry := v.(map[string]interface{})
 | 
				
			||||||
 | 
							s := slack.Attachment{
 | 
				
			||||||
 | 
								Fallback:   extractStringField(entry, "fallback"),
 | 
				
			||||||
 | 
								Color:      extractStringField(entry, "color"),
 | 
				
			||||||
 | 
								Pretext:    extractStringField(entry, "pretext"),
 | 
				
			||||||
 | 
								AuthorName: extractStringField(entry, "author_name"),
 | 
				
			||||||
 | 
								AuthorLink: extractStringField(entry, "author_link"),
 | 
				
			||||||
 | 
								AuthorIcon: extractStringField(entry, "author_icon"),
 | 
				
			||||||
 | 
								Title:      extractStringField(entry, "title"),
 | 
				
			||||||
 | 
								TitleLink:  extractStringField(entry, "title_link"),
 | 
				
			||||||
 | 
								Text:       extractStringField(entry, "text"),
 | 
				
			||||||
 | 
								ImageURL:   extractStringField(entry, "image_url"),
 | 
				
			||||||
 | 
								ThumbURL:   extractStringField(entry, "thumb_url"),
 | 
				
			||||||
 | 
								Footer:     extractStringField(entry, "footer"),
 | 
				
			||||||
 | 
								FooterIcon: extractStringField(entry, "footer_icon"),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							attachements = append(attachements, s)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return attachements
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func extractStringField(data map[string]interface{}, field string) string {
 | 
				
			||||||
 | 
						if rawValue, found := data[field]; found {
 | 
				
			||||||
 | 
							if value, ok := rawValue.(string); ok {
 | 
				
			||||||
 | 
								return value
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										343
									
								
								bridge/slack/users_channels.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								bridge/slack/users_channels.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,343 @@
 | 
				
			|||||||
 | 
					package bslack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"github.com/slack-go/slack"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const minimumRefreshInterval = 10 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type users struct {
 | 
				
			||||||
 | 
						log *logrus.Entry
 | 
				
			||||||
 | 
						sc  *slack.Client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						users           map[string]*slack.User
 | 
				
			||||||
 | 
						usersMutex      sync.RWMutex
 | 
				
			||||||
 | 
						usersSyncPoints map[string]chan struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						refreshInProgress bool
 | 
				
			||||||
 | 
						earliestRefresh   time.Time
 | 
				
			||||||
 | 
						refreshMutex      sync.Mutex
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newUserManager(log *logrus.Entry, sc *slack.Client) *users {
 | 
				
			||||||
 | 
						return &users{
 | 
				
			||||||
 | 
							log:             log,
 | 
				
			||||||
 | 
							sc:              sc,
 | 
				
			||||||
 | 
							users:           make(map[string]*slack.User),
 | 
				
			||||||
 | 
							usersSyncPoints: make(map[string]chan struct{}),
 | 
				
			||||||
 | 
							earliestRefresh: time.Now(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *users) getUser(id string) *slack.User {
 | 
				
			||||||
 | 
						b.usersMutex.RLock()
 | 
				
			||||||
 | 
						user, ok := b.users[id]
 | 
				
			||||||
 | 
						b.usersMutex.RUnlock()
 | 
				
			||||||
 | 
						if ok {
 | 
				
			||||||
 | 
							return user
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.populateUser(id)
 | 
				
			||||||
 | 
						b.usersMutex.RLock()
 | 
				
			||||||
 | 
						defer b.usersMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.users[id]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *users) getUsername(id string) string {
 | 
				
			||||||
 | 
						if user := b.getUser(id); user != nil {
 | 
				
			||||||
 | 
							if user.Profile.DisplayName != "" {
 | 
				
			||||||
 | 
								return user.Profile.DisplayName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return user.Name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.log.Warnf("Could not find user with ID '%s'", id)
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *users) getAvatar(id string) string {
 | 
				
			||||||
 | 
						if user := b.getUser(id); user != nil {
 | 
				
			||||||
 | 
							return user.Profile.Image48
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *users) populateUser(userID string) {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							b.usersMutex.Lock()
 | 
				
			||||||
 | 
							_, exists := b.users[userID]
 | 
				
			||||||
 | 
							if exists {
 | 
				
			||||||
 | 
								// already in cache
 | 
				
			||||||
 | 
								b.usersMutex.Unlock()
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if syncPoint, ok := b.usersSyncPoints[userID]; ok {
 | 
				
			||||||
 | 
								// Another goroutine is already populating this user for us so wait on it to finish.
 | 
				
			||||||
 | 
								b.usersMutex.Unlock()
 | 
				
			||||||
 | 
								<-syncPoint
 | 
				
			||||||
 | 
								// We do not return and iterate again to check that the entry does indeed exist
 | 
				
			||||||
 | 
								// in case the previous query failed for some reason.
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								b.usersSyncPoints[userID] = make(chan struct{})
 | 
				
			||||||
 | 
								defer func() {
 | 
				
			||||||
 | 
									// Wake up any waiting goroutines and remove the synchronization point.
 | 
				
			||||||
 | 
									close(b.usersSyncPoints[userID])
 | 
				
			||||||
 | 
									delete(b.usersSyncPoints, userID)
 | 
				
			||||||
 | 
								}()
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Do not hold the lock while fetching information from Slack
 | 
				
			||||||
 | 
						// as this might take an unbounded amount of time.
 | 
				
			||||||
 | 
						b.usersMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, err := b.sc.GetUserInfo(userID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.log.Debugf("GetUserInfo failed for %v: %v", userID, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.usersMutex.Lock()
 | 
				
			||||||
 | 
						defer b.usersMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Register user information.
 | 
				
			||||||
 | 
						b.users[userID] = user
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *users) invalidateUser(userID string) {
 | 
				
			||||||
 | 
						b.usersMutex.Lock()
 | 
				
			||||||
 | 
						defer b.usersMutex.Unlock()
 | 
				
			||||||
 | 
						delete(b.users, userID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *users) populateUsers(wait bool) {
 | 
				
			||||||
 | 
						b.refreshMutex.Lock()
 | 
				
			||||||
 | 
						if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
 | 
				
			||||||
 | 
							b.log.Debugf("Not refreshing user list as it was done less than %v ago.", minimumRefreshInterval)
 | 
				
			||||||
 | 
							b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for b.refreshInProgress {
 | 
				
			||||||
 | 
							b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
							time.Sleep(time.Second)
 | 
				
			||||||
 | 
							b.refreshMutex.Lock()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.refreshInProgress = true
 | 
				
			||||||
 | 
						b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newUsers := map[string]*slack.User{}
 | 
				
			||||||
 | 
						pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
 | 
				
			||||||
 | 
						count := 0
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							pagination, err = pagination.Next(context.Background())
 | 
				
			||||||
 | 
							time.Sleep(time.Second)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if pagination.Done(err) {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err = handleRateLimit(b.log, err); err != nil {
 | 
				
			||||||
 | 
									b.log.Errorf("Could not retrieve users: %#v", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for i := range pagination.Users {
 | 
				
			||||||
 | 
								newUsers[pagination.Users[i].ID] = &pagination.Users[i]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.log.Debugf("getting %d users", len(pagination.Users))
 | 
				
			||||||
 | 
							count++
 | 
				
			||||||
 | 
							// more > 2000 users, slack will complain and ratelimit. break
 | 
				
			||||||
 | 
							if count > 10 {
 | 
				
			||||||
 | 
								b.log.Info("Large slack detected > 2000 users, skipping loading complete userlist.")
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.usersMutex.Lock()
 | 
				
			||||||
 | 
						defer b.usersMutex.Unlock()
 | 
				
			||||||
 | 
						b.users = newUsers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.refreshMutex.Lock()
 | 
				
			||||||
 | 
						defer b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
						b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
 | 
				
			||||||
 | 
						b.refreshInProgress = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type channels struct {
 | 
				
			||||||
 | 
						log *logrus.Entry
 | 
				
			||||||
 | 
						sc  *slack.Client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channelsByID   map[string]*slack.Channel
 | 
				
			||||||
 | 
						channelsByName map[string]*slack.Channel
 | 
				
			||||||
 | 
						channelsMutex  sync.RWMutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						channelMembers      map[string][]string
 | 
				
			||||||
 | 
						channelMembersMutex sync.RWMutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						refreshInProgress bool
 | 
				
			||||||
 | 
						earliestRefresh   time.Time
 | 
				
			||||||
 | 
						refreshMutex      sync.Mutex
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newChannelManager(log *logrus.Entry, sc *slack.Client) *channels {
 | 
				
			||||||
 | 
						return &channels{
 | 
				
			||||||
 | 
							log:             log,
 | 
				
			||||||
 | 
							sc:              sc,
 | 
				
			||||||
 | 
							channelsByID:    make(map[string]*slack.Channel),
 | 
				
			||||||
 | 
							channelsByName:  make(map[string]*slack.Channel),
 | 
				
			||||||
 | 
							earliestRefresh: time.Now(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *channels) getChannel(channel string) (*slack.Channel, error) {
 | 
				
			||||||
 | 
						if strings.HasPrefix(channel, "ID:") {
 | 
				
			||||||
 | 
							return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return b.getChannelByName(channel)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *channels) getChannelByName(name string) (*slack.Channel, error) {
 | 
				
			||||||
 | 
						return b.getChannelBy(name, b.channelsByName)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *channels) getChannelByID(id string) (*slack.Channel, error) {
 | 
				
			||||||
 | 
						return b.getChannelBy(id, b.channelsByID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *channels) getChannelBy(lookupKey string, lookupMap map[string]*slack.Channel) (*slack.Channel, error) {
 | 
				
			||||||
 | 
						b.channelsMutex.RLock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if channel, ok := lookupMap[lookupKey]; ok {
 | 
				
			||||||
 | 
							return channel, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, fmt.Errorf("channel %s not found", lookupKey)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *channels) getChannelMembers(users *users) config.ChannelMembers {
 | 
				
			||||||
 | 
						b.channelMembersMutex.RLock()
 | 
				
			||||||
 | 
						defer b.channelMembersMutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						membersInfo := config.ChannelMembers{}
 | 
				
			||||||
 | 
						for channelID, members := range b.channelMembers {
 | 
				
			||||||
 | 
							for _, member := range members {
 | 
				
			||||||
 | 
								channelName := ""
 | 
				
			||||||
 | 
								userName := ""
 | 
				
			||||||
 | 
								userNick := ""
 | 
				
			||||||
 | 
								user := users.getUser(member)
 | 
				
			||||||
 | 
								if user != nil {
 | 
				
			||||||
 | 
									userName = user.Name
 | 
				
			||||||
 | 
									userNick = user.Profile.DisplayName
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								channel, _ := b.getChannelByID(channelID)
 | 
				
			||||||
 | 
								if channel != nil {
 | 
				
			||||||
 | 
									channelName = channel.Name
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								memberInfo := config.ChannelMember{
 | 
				
			||||||
 | 
									Username:    userName,
 | 
				
			||||||
 | 
									Nick:        userNick,
 | 
				
			||||||
 | 
									UserID:      member,
 | 
				
			||||||
 | 
									ChannelID:   channelID,
 | 
				
			||||||
 | 
									ChannelName: channelName,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								membersInfo = append(membersInfo, memberInfo)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return membersInfo
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *channels) registerChannel(channel slack.Channel) {
 | 
				
			||||||
 | 
						b.channelsMutex.Lock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.channelsByID[channel.ID] = &channel
 | 
				
			||||||
 | 
						b.channelsByName[channel.Name] = &channel
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *channels) populateChannels(wait bool) {
 | 
				
			||||||
 | 
						b.refreshMutex.Lock()
 | 
				
			||||||
 | 
						if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
 | 
				
			||||||
 | 
							b.log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.", minimumRefreshInterval)
 | 
				
			||||||
 | 
							b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for b.refreshInProgress {
 | 
				
			||||||
 | 
							b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
							time.Sleep(time.Second)
 | 
				
			||||||
 | 
							b.refreshMutex.Lock()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.refreshInProgress = true
 | 
				
			||||||
 | 
						b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newChannelsByID := map[string]*slack.Channel{}
 | 
				
			||||||
 | 
						newChannelsByName := map[string]*slack.Channel{}
 | 
				
			||||||
 | 
						newChannelMembers := make(map[string][]string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We only retrieve public and private channels, not IMs
 | 
				
			||||||
 | 
						// and MPIMs as those do not have a channel name.
 | 
				
			||||||
 | 
						queryParams := &slack.GetConversationsParameters{
 | 
				
			||||||
 | 
							ExcludeArchived: true,
 | 
				
			||||||
 | 
							Types:           []string{"public_channel,private_channel"},
 | 
				
			||||||
 | 
							Limit:           1000,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							channels, nextCursor, err := b.sc.GetConversations(queryParams)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if err = handleRateLimit(b.log, err); err != nil {
 | 
				
			||||||
 | 
									b.log.Errorf("Could not retrieve channels: %#v", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for i := range channels {
 | 
				
			||||||
 | 
								newChannelsByID[channels[i].ID] = &channels[i]
 | 
				
			||||||
 | 
								newChannelsByName[channels[i].Name] = &channels[i]
 | 
				
			||||||
 | 
								// also find all the members in every channel
 | 
				
			||||||
 | 
								// comment for now, issues on big slacks
 | 
				
			||||||
 | 
								/*
 | 
				
			||||||
 | 
									members, err := b.getUsersInConversation(channels[i].ID)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										if err = b.handleRateLimit(err); err != nil {
 | 
				
			||||||
 | 
											b.Log.Errorf("Could not retrieve channel members: %#v", err)
 | 
				
			||||||
 | 
											return
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									newChannelMembers[channels[i].ID] = members
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if nextCursor == "" {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							queryParams.Cursor = nextCursor
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.channelsMutex.Lock()
 | 
				
			||||||
 | 
						defer b.channelsMutex.Unlock()
 | 
				
			||||||
 | 
						b.channelsByID = newChannelsByID
 | 
				
			||||||
 | 
						b.channelsByName = newChannelsByName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.channelMembersMutex.Lock()
 | 
				
			||||||
 | 
						defer b.channelMembersMutex.Unlock()
 | 
				
			||||||
 | 
						b.channelMembers = newChannelMembers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.refreshMutex.Lock()
 | 
				
			||||||
 | 
						defer b.refreshMutex.Unlock()
 | 
				
			||||||
 | 
						b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
 | 
				
			||||||
 | 
						b.refreshInProgress = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										169
									
								
								bridge/sshchat/sshchat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								bridge/sshchat/sshchat.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,169 @@
 | 
				
			|||||||
 | 
					package bsshchat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/shazow/ssh-chat/sshd"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bsshchat struct {
 | 
				
			||||||
 | 
						r *bufio.Scanner
 | 
				
			||||||
 | 
						w io.WriteCloser
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						return &Bsshchat{Config: cfg}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsshchat) Connect() error {
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// connHandler will be called by 'sshd.ConnectShell()' below
 | 
				
			||||||
 | 
						// once the connection is established in order to handle it.
 | 
				
			||||||
 | 
						connErr := make(chan error, 1) // Needs to be buffered.
 | 
				
			||||||
 | 
						connSignal := make(chan struct{})
 | 
				
			||||||
 | 
						connHandler := func(r io.Reader, w io.WriteCloser) error {
 | 
				
			||||||
 | 
							b.r = bufio.NewScanner(r)
 | 
				
			||||||
 | 
							b.r.Scan()
 | 
				
			||||||
 | 
							b.w = w
 | 
				
			||||||
 | 
							if _, err := b.w.Write([]byte("/theme mono\r\n/quiet\r\n")); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							close(connSignal) // Connection is established so we can signal the success.
 | 
				
			||||||
 | 
							return b.handleSSHChat()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							// As a successful connection will result in this returning after the Connection
 | 
				
			||||||
 | 
							// method has already returned point we NEED to have a buffered channel to still
 | 
				
			||||||
 | 
							// be able to write.
 | 
				
			||||||
 | 
							connErr <- sshd.ConnectShell(b.GetString("Server"), b.GetString("Nick"), connHandler)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case err := <-connErr:
 | 
				
			||||||
 | 
							b.Log.Error("Connection failed")
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						case <-connSignal:
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsshchat) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsshchat) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsshchat) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// ignore delete messages
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								if _, err := b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n")); err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("Could not send extra message: %#v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return b.handleUploadFile(&msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
 | 
				
			||||||
 | 
						return "", err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					func (b *Bsshchat) sshchatKeepAlive() chan bool {
 | 
				
			||||||
 | 
						done := make(chan bool)
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							ticker := time.NewTicker(90 * time.Second)
 | 
				
			||||||
 | 
							defer ticker.Stop()
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								select {
 | 
				
			||||||
 | 
								case <-ticker.C:
 | 
				
			||||||
 | 
									b.Log.Debugf("PING")
 | 
				
			||||||
 | 
									err := b.xc.PingC2S("", "")
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										b.Log.Debugf("PING failed %#v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								case <-done:
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						return done
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func stripPrompt(s string) string {
 | 
				
			||||||
 | 
						pos := strings.LastIndex(s, "\033[K")
 | 
				
			||||||
 | 
						if pos < 0 {
 | 
				
			||||||
 | 
							return s
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s[pos+3:]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsshchat) handleSSHChat() error {
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							done := b.sshchatKeepAlive()
 | 
				
			||||||
 | 
							defer close(done)
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
						wait := true
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							if b.r.Scan() {
 | 
				
			||||||
 | 
								// ignore messages from ourselves
 | 
				
			||||||
 | 
								if !strings.Contains(b.r.Text(), "\033[K") {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if strings.Contains(b.r.Text(), "Rate limiting is in effect") {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// skip our own messages
 | 
				
			||||||
 | 
								if !strings.HasPrefix(b.r.Text(), "["+b.GetString("Nick")+"] \x1b") {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								res := strings.Split(stripPrompt(b.r.Text()), ":")
 | 
				
			||||||
 | 
								if res[0] == "-> Set theme" {
 | 
				
			||||||
 | 
									wait = false
 | 
				
			||||||
 | 
									b.Log.Debugf("mono found, allowing")
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !wait {
 | 
				
			||||||
 | 
									b.Log.Debugf("<= Message %#v", res)
 | 
				
			||||||
 | 
									rmsg := config.Message{Username: res[0], Text: strings.TrimSpace(strings.Join(res[1:], ":")), Channel: "sshchat", Account: b.Account, UserID: "nick"}
 | 
				
			||||||
 | 
									b.Remote <- rmsg
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsshchat) handleUploadFile(msg *config.Message) (string, error) {
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								msg.Text += fi.Comment + ": "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fi.URL != "" {
 | 
				
			||||||
 | 
								msg.Text = fi.URL
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									msg.Text = fi.Comment + ": " + fi.URL
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if _, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n")); err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Could not send file message: %#v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										126
									
								
								bridge/steam/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								bridge/steam/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					package bsteam
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/Philipp15b/go-steam"
 | 
				
			||||||
 | 
						"github.com/Philipp15b/go-steam/protocol/steamlang"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) handleChatMsg(e *steam.ChatMsgEvent) {
 | 
				
			||||||
 | 
						b.Log.Debugf("Receiving ChatMsgEvent: %#v", e)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
 | 
				
			||||||
 | 
						var channel int64
 | 
				
			||||||
 | 
						if e.ChatRoomId == 0 {
 | 
				
			||||||
 | 
							channel = int64(e.ChatterId)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// for some reason we have to remove 0x18000000000000
 | 
				
			||||||
 | 
							// TODO
 | 
				
			||||||
 | 
							// https://github.com/42wim/matterbridge/pull/630#discussion_r238102751
 | 
				
			||||||
 | 
							// channel = int64(e.ChatRoomId) & 0xfffffffffffff
 | 
				
			||||||
 | 
							channel = int64(e.ChatRoomId) - 0x18000000000000
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						msg := config.Message{
 | 
				
			||||||
 | 
							Username: b.getNick(e.ChatterId),
 | 
				
			||||||
 | 
							Text:     e.Message,
 | 
				
			||||||
 | 
							Channel:  strconv.FormatInt(channel, 10),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							UserID:   strconv.FormatInt(int64(e.ChatterId), 10),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Remote <- msg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) handleEvents() {
 | 
				
			||||||
 | 
						myLoginInfo := &steam.LogOnDetails{
 | 
				
			||||||
 | 
							Username: b.GetString("Login"),
 | 
				
			||||||
 | 
							Password: b.GetString("Password"),
 | 
				
			||||||
 | 
							AuthCode: b.GetString("AuthCode"),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// TODO Attempt to read existing auth hash to avoid steam guard.
 | 
				
			||||||
 | 
						// Maybe works
 | 
				
			||||||
 | 
						//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
 | 
				
			||||||
 | 
						for event := range b.c.Events() {
 | 
				
			||||||
 | 
							switch e := event.(type) {
 | 
				
			||||||
 | 
							case *steam.ChatMsgEvent:
 | 
				
			||||||
 | 
								b.handleChatMsg(e)
 | 
				
			||||||
 | 
							case *steam.PersonaStateEvent:
 | 
				
			||||||
 | 
								b.Log.Debugf("PersonaStateEvent: %#v\n", e)
 | 
				
			||||||
 | 
								b.Lock()
 | 
				
			||||||
 | 
								b.userMap[e.FriendId] = e.Name
 | 
				
			||||||
 | 
								b.Unlock()
 | 
				
			||||||
 | 
							case *steam.ConnectedEvent:
 | 
				
			||||||
 | 
								b.c.Auth.LogOn(myLoginInfo)
 | 
				
			||||||
 | 
							case *steam.MachineAuthUpdateEvent:
 | 
				
			||||||
 | 
							// TODO sentry files for 2 auth
 | 
				
			||||||
 | 
							/*
 | 
				
			||||||
 | 
								b.Log.Info("authupdate", e)
 | 
				
			||||||
 | 
								b.Log.Info("hash", e.Hash)
 | 
				
			||||||
 | 
								ioutil.WriteFile("sentry", e.Hash, 0666)
 | 
				
			||||||
 | 
							*/
 | 
				
			||||||
 | 
							case *steam.LogOnFailedEvent:
 | 
				
			||||||
 | 
								b.Log.Info("Logon failed", e)
 | 
				
			||||||
 | 
								err := b.handleLogOnFailed(e, myLoginInfo)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Error(err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case *steam.LoggedOnEvent:
 | 
				
			||||||
 | 
								b.Log.Debugf("LoggedOnEvent: %#v", e)
 | 
				
			||||||
 | 
								b.connected <- struct{}{}
 | 
				
			||||||
 | 
								b.Log.Debugf("setting online")
 | 
				
			||||||
 | 
								b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
 | 
				
			||||||
 | 
							case *steam.DisconnectedEvent:
 | 
				
			||||||
 | 
								b.Log.Info("Disconnected")
 | 
				
			||||||
 | 
								b.Log.Info("Attempting to reconnect...")
 | 
				
			||||||
 | 
								b.c.Connect()
 | 
				
			||||||
 | 
							case steam.FatalErrorEvent:
 | 
				
			||||||
 | 
								b.Log.Errorf("steam FatalErrorEvent: %#v", e)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								b.Log.Debugf("unknown event %#v", e)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error {
 | 
				
			||||||
 | 
						switch e.Result {
 | 
				
			||||||
 | 
						case steamlang.EResult_AccountLoginDeniedNeedTwoFactor:
 | 
				
			||||||
 | 
							b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
 | 
				
			||||||
 | 
							var code string
 | 
				
			||||||
 | 
							fmt.Scanf("%s", &code)
 | 
				
			||||||
 | 
							// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
 | 
				
			||||||
 | 
							myLoginInfo.TwoFactorCode = code
 | 
				
			||||||
 | 
						case steamlang.EResult_AccountLogonDenied:
 | 
				
			||||||
 | 
							b.Log.Info("Steam guard isn't letting me in! Enter auth code:")
 | 
				
			||||||
 | 
							var code string
 | 
				
			||||||
 | 
							fmt.Scanf("%s", &code)
 | 
				
			||||||
 | 
							// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
 | 
				
			||||||
 | 
							myLoginInfo.AuthCode = code
 | 
				
			||||||
 | 
						case steamlang.EResult_InvalidLoginAuthCode:
 | 
				
			||||||
 | 
							return fmt.Errorf("Steam guard: invalid login auth code: %#v ", e.Result)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return fmt.Errorf("LogOnFailedEvent: %#v ", e.Result)
 | 
				
			||||||
 | 
							// TODO: Handle EResult_InvalidLoginAuthCode
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleFileInfo handles config.FileInfo and adds correct file comment or URL to msg.Text.
 | 
				
			||||||
 | 
					// Returns error if cast fails.
 | 
				
			||||||
 | 
					func (b *Bsteam) handleFileInfo(msg *config.Message, f interface{}) error {
 | 
				
			||||||
 | 
						if _, ok := f.(config.FileInfo); !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("handleFileInfo cast failed %#v", f)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
						if fi.Comment != "" {
 | 
				
			||||||
 | 
							msg.Text += fi.Comment + ": "
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if fi.URL != "" {
 | 
				
			||||||
 | 
							msg.Text = fi.URL
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								msg.Text = fi.Comment + ": " + fi.URL
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										95
									
								
								bridge/steam/steam.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								bridge/steam/steam.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
				
			|||||||
 | 
					package bsteam
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/Philipp15b/go-steam"
 | 
				
			||||||
 | 
						"github.com/Philipp15b/go-steam/protocol/steamlang"
 | 
				
			||||||
 | 
						"github.com/Philipp15b/go-steam/steamid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bsteam struct {
 | 
				
			||||||
 | 
						c         *steam.Client
 | 
				
			||||||
 | 
						connected chan struct{}
 | 
				
			||||||
 | 
						userMap   map[steamid.SteamId]string
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						b := &Bsteam{Config: cfg}
 | 
				
			||||||
 | 
						b.userMap = make(map[steamid.SteamId]string)
 | 
				
			||||||
 | 
						b.connected = make(chan struct{})
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) Connect() error {
 | 
				
			||||||
 | 
						b.Log.Info("Connecting")
 | 
				
			||||||
 | 
						b.c = steam.NewClient()
 | 
				
			||||||
 | 
						go b.handleEvents()
 | 
				
			||||||
 | 
						go b.c.Connect()
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case <-b.connected:
 | 
				
			||||||
 | 
							b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						case <-time.After(time.Second * 30):
 | 
				
			||||||
 | 
							return fmt.Errorf("connection timed out")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) Disconnect() error {
 | 
				
			||||||
 | 
						b.c.Disconnect()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						id, err := steamid.NewId(channel.Name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.c.Social.JoinChat(id)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// ignore delete messages
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						id, err := steamid.NewId(msg.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle files
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, rmsg.Username+rmsg.Text)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for i := range msg.Extra["file"] {
 | 
				
			||||||
 | 
								if err := b.handleFileInfo(&msg, msg.Extra["file"][i]); err != nil {
 | 
				
			||||||
 | 
									b.Log.Error(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bsteam) getNick(id steamid.SteamId) string {
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						defer b.RUnlock()
 | 
				
			||||||
 | 
						if name, ok := b.userMap[id]; ok {
 | 
				
			||||||
 | 
							return name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "unknown"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										575
									
								
								bridge/telegram/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										575
									
								
								bridge/telegram/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,575 @@
 | 
				
			|||||||
 | 
					package btelegram
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"unicode/utf16"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/davecgh/go-spew/spew"
 | 
				
			||||||
 | 
						tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
 | 
				
			||||||
 | 
						// handle channels
 | 
				
			||||||
 | 
						if posted != nil {
 | 
				
			||||||
 | 
							if posted.Text == "/chatId" {
 | 
				
			||||||
 | 
								chatID := strconv.FormatInt(posted.Chat.ID, 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, err := b.Send(config.Message{
 | 
				
			||||||
 | 
									Channel: chatID,
 | 
				
			||||||
 | 
									Text:    fmt.Sprintf("ID of this chat: %s", chatID),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Warnf("Unable to send chatID to %s", chatID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								message = posted
 | 
				
			||||||
 | 
								rmsg.Text = message.Text
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// edited channel message
 | 
				
			||||||
 | 
						if edited != nil && !b.GetBool("EditDisable") {
 | 
				
			||||||
 | 
							message = edited
 | 
				
			||||||
 | 
							rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return message
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleChannels checks if it's a channel message and if the message is a new or edited messages
 | 
				
			||||||
 | 
					func (b *Btelegram) handleChannels(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
 | 
				
			||||||
 | 
						return b.handleUpdate(rmsg, message, update.ChannelPost, update.EditedChannelPost)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleGroups checks if it's a group message and if the message is a new or edited messages
 | 
				
			||||||
 | 
					func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
 | 
				
			||||||
 | 
						return b.handleUpdate(rmsg, message, update.Message, update.EditedMessage)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleForwarded handles forwarded messages
 | 
				
			||||||
 | 
					func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
 | 
				
			||||||
 | 
						if message.ForwardDate == 0 {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if message.ForwardFromChat != nil && message.ForwardFrom == nil {
 | 
				
			||||||
 | 
							rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if message.ForwardFrom == nil {
 | 
				
			||||||
 | 
							rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						usernameForward := ""
 | 
				
			||||||
 | 
						if b.GetBool("UseFirstName") {
 | 
				
			||||||
 | 
							usernameForward = message.ForwardFrom.FirstName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetBool("UseFullName") {
 | 
				
			||||||
 | 
							usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if usernameForward == "" {
 | 
				
			||||||
 | 
							usernameForward = message.ForwardFrom.UserName
 | 
				
			||||||
 | 
							if usernameForward == "" {
 | 
				
			||||||
 | 
								usernameForward = message.ForwardFrom.FirstName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if usernameForward == "" {
 | 
				
			||||||
 | 
							usernameForward = unknownUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleQuoting handles quoting of previous messages
 | 
				
			||||||
 | 
					func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
 | 
				
			||||||
 | 
						if message.ReplyToMessage != nil {
 | 
				
			||||||
 | 
							usernameReply := ""
 | 
				
			||||||
 | 
							if message.ReplyToMessage.From != nil {
 | 
				
			||||||
 | 
								if b.GetBool("UseFirstName") {
 | 
				
			||||||
 | 
									usernameReply = message.ReplyToMessage.From.FirstName
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if b.GetBool("UseFullName") {
 | 
				
			||||||
 | 
									usernameReply = message.ReplyToMessage.From.FirstName + " " + message.ReplyToMessage.From.LastName
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if usernameReply == "" {
 | 
				
			||||||
 | 
									usernameReply = message.ReplyToMessage.From.UserName
 | 
				
			||||||
 | 
									if usernameReply == "" {
 | 
				
			||||||
 | 
										usernameReply = message.ReplyToMessage.From.FirstName
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if usernameReply == "" {
 | 
				
			||||||
 | 
								usernameReply = unknownUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !b.GetBool("QuoteDisable") {
 | 
				
			||||||
 | 
								quote := message.ReplyToMessage.Text
 | 
				
			||||||
 | 
								if quote == "" {
 | 
				
			||||||
 | 
									quote = message.ReplyToMessage.Caption
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleUsername handles the correct setting of the username
 | 
				
			||||||
 | 
					func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
 | 
				
			||||||
 | 
						if message.From != nil {
 | 
				
			||||||
 | 
							rmsg.UserID = strconv.FormatInt(message.From.ID, 10)
 | 
				
			||||||
 | 
							if b.GetBool("UseFirstName") {
 | 
				
			||||||
 | 
								rmsg.Username = message.From.FirstName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if b.GetBool("UseFullName") {
 | 
				
			||||||
 | 
								rmsg.Username = message.From.FirstName + " " + message.From.LastName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if rmsg.Username == "" {
 | 
				
			||||||
 | 
								rmsg.Username = message.From.UserName
 | 
				
			||||||
 | 
								if rmsg.Username == "" {
 | 
				
			||||||
 | 
									rmsg.Username = message.From.FirstName
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// only download avatars if we have a place to upload them (configured mediaserver)
 | 
				
			||||||
 | 
							if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
 | 
				
			||||||
 | 
								b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if message.SenderChat != nil { //nolint:nestif
 | 
				
			||||||
 | 
							rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10)
 | 
				
			||||||
 | 
							if b.GetBool("UseFirstName") {
 | 
				
			||||||
 | 
								rmsg.Username = message.SenderChat.FirstName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if b.GetBool("UseFullName") {
 | 
				
			||||||
 | 
								rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
 | 
				
			||||||
 | 
								rmsg.Username = message.SenderChat.UserName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
 | 
				
			||||||
 | 
									rmsg.Username = message.SenderChat.FirstName
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// only download avatars if we have a place to upload them (configured mediaserver)
 | 
				
			||||||
 | 
							if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
 | 
				
			||||||
 | 
								b.handleDownloadAvatar(message.SenderChat.ID, rmsg.Channel)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if we really didn't find a username, set it to unknown
 | 
				
			||||||
 | 
						if rmsg.Username == "" {
 | 
				
			||||||
 | 
							rmsg.Username = unknownUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
				
			||||||
 | 
						for update := range updates {
 | 
				
			||||||
 | 
							b.Log.Debugf("== Receiving event: %#v", update.Message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if update.Message == nil && update.ChannelPost == nil &&
 | 
				
			||||||
 | 
								update.EditedMessage == nil && update.EditedChannelPost == nil {
 | 
				
			||||||
 | 
								b.Log.Error("Getting nil messages, this shouldn't happen.")
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.GetInt("debuglevel") == 1 {
 | 
				
			||||||
 | 
								spew.Dump(update.Message)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var message *tgbotapi.Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle channels
 | 
				
			||||||
 | 
							message = b.handleChannels(&rmsg, message, update)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle groups
 | 
				
			||||||
 | 
							message = b.handleGroups(&rmsg, message, update)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if message == nil {
 | 
				
			||||||
 | 
								b.Log.Error("message is nil, this shouldn't happen.")
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// set the ID's from the channel or group message
 | 
				
			||||||
 | 
							rmsg.ID = strconv.Itoa(message.MessageID)
 | 
				
			||||||
 | 
							rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// preserve threading from telegram reply
 | 
				
			||||||
 | 
							if message.ReplyToMessage != nil {
 | 
				
			||||||
 | 
								rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle entities (adding URLs)
 | 
				
			||||||
 | 
							b.handleEntities(&rmsg, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle username
 | 
				
			||||||
 | 
							b.handleUsername(&rmsg, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle any downloads
 | 
				
			||||||
 | 
							err := b.handleDownload(&rmsg, message)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("download failed: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// handle forwarded messages
 | 
				
			||||||
 | 
							b.handleForwarded(&rmsg, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// quote the previous message
 | 
				
			||||||
 | 
							b.handleQuoting(&rmsg, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if rmsg.Text != "" || len(rmsg.Extra) > 0 {
 | 
				
			||||||
 | 
								// Comment the next line out due to avoid removing empty lines in Telegram
 | 
				
			||||||
 | 
								// rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
 | 
				
			||||||
 | 
								// channels don't have (always?) user information. see #410
 | 
				
			||||||
 | 
								if message.From != nil {
 | 
				
			||||||
 | 
									rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
								b.Remote <- rmsg
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadAvatar downloads the avatar of userid from channel
 | 
				
			||||||
 | 
					// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
 | 
				
			||||||
 | 
					// logs an error message if it fails
 | 
				
			||||||
 | 
					func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							Username: "system",
 | 
				
			||||||
 | 
							Text:     "avatar",
 | 
				
			||||||
 | 
							Channel:  channel,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							UserID:   strconv.FormatInt(userid, 10),
 | 
				
			||||||
 | 
							Event:    config.EventAvatarDownload,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(photos.Photos) > 0 {
 | 
				
			||||||
 | 
							photo := photos.Photos[0][0]
 | 
				
			||||||
 | 
							url := b.getFileDirectURL(photo.FileID)
 | 
				
			||||||
 | 
							name := strconv.FormatInt(userid, 10) + ".png"
 | 
				
			||||||
 | 
							b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Error(err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							data, err := helper.DownloadFile(url)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("download %s failed %#v", url, err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
 | 
				
			||||||
 | 
							b.Remote <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
 | 
				
			||||||
 | 
						format := b.GetString("MediaConvertTgs")
 | 
				
			||||||
 | 
						if helper.SupportsFormat(format) {
 | 
				
			||||||
 | 
							b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Otherwise, no conversion was requested. Trying to run the usual webp
 | 
				
			||||||
 | 
							// converter would fail, because '.tgs.webp' is actually a gzipped JSON
 | 
				
			||||||
 | 
							// file, and has nothing to do with WebP.
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err := helper.ConvertTgsToX(data, format, b.Log)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("conversion failed: %v", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							*name = strings.Replace(*name, "tgs.webp", format, 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
 | 
				
			||||||
 | 
						if b.GetBool("MediaConvertWebPToPNG") {
 | 
				
			||||||
 | 
							b.Log.Debugf("WebP to PNG conversion enabled, converting %v", name)
 | 
				
			||||||
 | 
							err := helper.ConvertWebPToPNG(data)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("conversion failed: %v", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								*name = strings.Replace(*name, ".webp", ".png", 1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadFile handles file download
 | 
				
			||||||
 | 
					func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
 | 
				
			||||||
 | 
						size := 0
 | 
				
			||||||
 | 
						var url, name, text string
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case message.Sticker != nil:
 | 
				
			||||||
 | 
							text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
 | 
				
			||||||
 | 
							size = message.Sticker.FileSize
 | 
				
			||||||
 | 
						case message.Voice != nil:
 | 
				
			||||||
 | 
							text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
 | 
				
			||||||
 | 
							size = message.Voice.FileSize
 | 
				
			||||||
 | 
						case message.Video != nil:
 | 
				
			||||||
 | 
							text, name, url = b.getDownloadInfo(message.Video.FileID, "", true)
 | 
				
			||||||
 | 
							size = message.Video.FileSize
 | 
				
			||||||
 | 
						case message.Audio != nil:
 | 
				
			||||||
 | 
							text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true)
 | 
				
			||||||
 | 
							size = message.Audio.FileSize
 | 
				
			||||||
 | 
						case message.Document != nil:
 | 
				
			||||||
 | 
							_, _, url = b.getDownloadInfo(message.Document.FileID, "", false)
 | 
				
			||||||
 | 
							size = message.Document.FileSize
 | 
				
			||||||
 | 
							name = message.Document.FileName
 | 
				
			||||||
 | 
							text = " " + message.Document.FileName + " : " + url
 | 
				
			||||||
 | 
						case message.Photo != nil:
 | 
				
			||||||
 | 
							photos := message.Photo
 | 
				
			||||||
 | 
							size = photos[len(photos)-1].FileSize
 | 
				
			||||||
 | 
							text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if name is empty we didn't match a thing to download
 | 
				
			||||||
 | 
						if name == "" {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// use the URL instead of native upload
 | 
				
			||||||
 | 
						if b.GetBool("UseInsecureURL") {
 | 
				
			||||||
 | 
							b.Log.Debugf("Setting message text to :%s", text)
 | 
				
			||||||
 | 
							rmsg.Text += text
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
 | 
				
			||||||
 | 
						err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						data, err := helper.DownloadFile(url)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if strings.HasSuffix(name, ".tgs.webp") {
 | 
				
			||||||
 | 
							b.maybeConvertTgs(&name, data)
 | 
				
			||||||
 | 
						} else if strings.HasSuffix(name, ".webp") {
 | 
				
			||||||
 | 
							b.maybeConvertWebp(&name, data)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// rename .oga to .ogg  https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512
 | 
				
			||||||
 | 
						if strings.HasSuffix(name, ".oga") && message.Audio != nil {
 | 
				
			||||||
 | 
							name = strings.Replace(name, ".oga", ".ogg", 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) {
 | 
				
			||||||
 | 
						url := b.getFileDirectURL(id)
 | 
				
			||||||
 | 
						name := ""
 | 
				
			||||||
 | 
						if urlpart {
 | 
				
			||||||
 | 
							urlPart := strings.Split(url, "/")
 | 
				
			||||||
 | 
							name = urlPart[len(urlPart)-1]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") {
 | 
				
			||||||
 | 
							name += suffix
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						text := " " + url
 | 
				
			||||||
 | 
						return text, name, url
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDelete handles message deleting
 | 
				
			||||||
 | 
					func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, error) {
 | 
				
			||||||
 | 
						if msg.ID == "" {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msgid, err := strconv.Atoi(msg.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
 | 
				
			||||||
 | 
						_, err = b.c.Request(cfg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "", err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleEdit handles message editing.
 | 
				
			||||||
 | 
					func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error) {
 | 
				
			||||||
 | 
						msgid, err := strconv.Atoi(msg.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode HTML - nick only")
 | 
				
			||||||
 | 
							msg.Text = html.EscapeString(msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
 | 
				
			||||||
 | 
						switch b.GetString("MessageFormat") {
 | 
				
			||||||
 | 
						case HTMLFormat:
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode HTML")
 | 
				
			||||||
 | 
							m.ParseMode = tgbotapi.ModeHTML
 | 
				
			||||||
 | 
						case "Markdown":
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode markdown")
 | 
				
			||||||
 | 
							m.ParseMode = tgbotapi.ModeMarkdown
 | 
				
			||||||
 | 
						case MarkdownV2:
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode MarkdownV2")
 | 
				
			||||||
 | 
							m.ParseMode = MarkdownV2
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode HTML - nick only")
 | 
				
			||||||
 | 
							m.ParseMode = tgbotapi.ModeHTML
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = b.c.Send(m)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleUploadFile handles native upload of files
 | 
				
			||||||
 | 
					func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, parentID int) (string, error) {
 | 
				
			||||||
 | 
						var media []interface{}
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							file := tgbotapi.FileBytes{
 | 
				
			||||||
 | 
								Name:  fi.Name,
 | 
				
			||||||
 | 
								Bytes: *fi.Data,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							switch filepath.Ext(fi.Name) {
 | 
				
			||||||
 | 
							case ".jpg", ".jpe", ".png":
 | 
				
			||||||
 | 
								pc := tgbotapi.NewInputMediaPhoto(file)
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								media = append(media, pc)
 | 
				
			||||||
 | 
							case ".mp4", ".m4v":
 | 
				
			||||||
 | 
								vc := tgbotapi.NewInputMediaVideo(file)
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								media = append(media, vc)
 | 
				
			||||||
 | 
							case ".mp3", ".oga":
 | 
				
			||||||
 | 
								ac := tgbotapi.NewInputMediaAudio(file)
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								media = append(media, ac)
 | 
				
			||||||
 | 
							case ".ogg":
 | 
				
			||||||
 | 
								voc := tgbotapi.NewVoice(chatid, file)
 | 
				
			||||||
 | 
								voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
				
			||||||
 | 
								voc.ReplyToMessageID = parentID
 | 
				
			||||||
 | 
								res, err := b.c.Send(voc)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return "", err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return strconv.Itoa(res.MessageID), nil
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								dc := tgbotapi.NewInputMediaDocument(file)
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								media = append(media, dc)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.sendMediaFiles(msg, chatid, parentID, media)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
 | 
				
			||||||
 | 
						format := b.GetString("quoteformat")
 | 
				
			||||||
 | 
						if format == "" {
 | 
				
			||||||
 | 
							format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						quoteMessagelength := len([]rune(quoteMessage))
 | 
				
			||||||
 | 
						if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
 | 
				
			||||||
 | 
							runes := []rune(quoteMessage)
 | 
				
			||||||
 | 
							quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
 | 
				
			||||||
 | 
							if quoteMessagelength > b.GetInt("QuoteLengthLimit") {
 | 
				
			||||||
 | 
								quoteMessage += "..."
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						format = strings.Replace(format, "{MESSAGE}", message, -1)
 | 
				
			||||||
 | 
						format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
 | 
				
			||||||
 | 
						format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
 | 
				
			||||||
 | 
						return format
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleEntities handles messageEntities
 | 
				
			||||||
 | 
					func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Message) {
 | 
				
			||||||
 | 
						if message.Entities == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						indexMovedBy := 0
 | 
				
			||||||
 | 
						prevLinkOffset := -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, e := range message.Entities {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							asRunes := utf16.Encode([]rune(rmsg.Text))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if e.Type == "text_link" {
 | 
				
			||||||
 | 
								offset := e.Offset + indexMovedBy
 | 
				
			||||||
 | 
								url, err := e.ParseURL()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("entity text_link url parse failed: %s", err)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								utfEncodedString := utf16.Encode([]rune(rmsg.Text))
 | 
				
			||||||
 | 
								if offset+e.Length > len(utfEncodedString) {
 | 
				
			||||||
 | 
									b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								rmsg.Text = string(utf16.Decode(asRunes[:offset+e.Length])) + " (" + url.String() + ")" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
				
			||||||
 | 
								indexMovedBy += len(url.String()) + 3
 | 
				
			||||||
 | 
								prevLinkOffset = e.Offset
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if e.Offset == prevLinkOffset {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if e.Type == "code" {
 | 
				
			||||||
 | 
								offset := e.Offset + indexMovedBy
 | 
				
			||||||
 | 
								rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "`" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "`" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
				
			||||||
 | 
								indexMovedBy += 2
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if e.Type == "pre" {
 | 
				
			||||||
 | 
								offset := e.Offset + indexMovedBy
 | 
				
			||||||
 | 
								rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "```\n" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "```\n" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
				
			||||||
 | 
								indexMovedBy += 8
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if e.Type == "bold" {
 | 
				
			||||||
 | 
								offset := e.Offset + indexMovedBy
 | 
				
			||||||
 | 
								rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "*" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "*" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
				
			||||||
 | 
								indexMovedBy += 2
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if e.Type == "italic" {
 | 
				
			||||||
 | 
								offset := e.Offset + indexMovedBy
 | 
				
			||||||
 | 
								rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "_" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "_" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
				
			||||||
 | 
								indexMovedBy += 2
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if e.Type == "strike" {
 | 
				
			||||||
 | 
								offset := e.Offset + indexMovedBy
 | 
				
			||||||
 | 
								rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "~" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "~" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
				
			||||||
 | 
								indexMovedBy += 2
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										65
									
								
								bridge/telegram/html.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								bridge/telegram/html.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					package btelegram
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/russross/blackfriday"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type customHTML struct {
 | 
				
			||||||
 | 
						blackfriday.Renderer
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) {
 | 
				
			||||||
 | 
						marker := out.Len()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !text() {
 | 
				
			||||||
 | 
							out.Truncate(marker)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						out.WriteString("\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) {
 | 
				
			||||||
 | 
						out.WriteString("<pre>")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						out.WriteString(html.EscapeString(string(text)))
 | 
				
			||||||
 | 
						out.WriteString("</pre>\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int, id string) {
 | 
				
			||||||
 | 
						options.Paragraph(out, text)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (options *customHTML) HRule(out *bytes.Buffer) {
 | 
				
			||||||
 | 
						out.WriteByte('\n') //nolint:errcheck
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
 | 
				
			||||||
 | 
						out.WriteString("> ")
 | 
				
			||||||
 | 
						out.Write(text)
 | 
				
			||||||
 | 
						out.WriteByte('\n')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (options *customHTML) List(out *bytes.Buffer, text func() bool, flags int) {
 | 
				
			||||||
 | 
						options.Paragraph(out, text)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (options *customHTML) ListItem(out *bytes.Buffer, text []byte, flags int) {
 | 
				
			||||||
 | 
						out.WriteString("- ")
 | 
				
			||||||
 | 
						out.Write(text)
 | 
				
			||||||
 | 
						out.WriteByte('\n')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func makeHTML(input string) string {
 | 
				
			||||||
 | 
						return string(blackfriday.Markdown([]byte(input),
 | 
				
			||||||
 | 
							&customHTML{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")},
 | 
				
			||||||
 | 
							blackfriday.EXTENSION_NO_INTRA_EMPHASIS|
 | 
				
			||||||
 | 
								blackfriday.EXTENSION_FENCED_CODE|
 | 
				
			||||||
 | 
								blackfriday.EXTENSION_AUTOLINK|
 | 
				
			||||||
 | 
								blackfriday.EXTENSION_SPACE_HEADERS|
 | 
				
			||||||
 | 
								blackfriday.EXTENSION_HEADER_IDS|
 | 
				
			||||||
 | 
								blackfriday.EXTENSION_BACKSLASH_LINE_BREAK|
 | 
				
			||||||
 | 
								blackfriday.EXTENSION_DEFINITION_LISTS))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										205
									
								
								bridge/telegram/telegram.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								bridge/telegram/telegram.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
				
			|||||||
 | 
					package btelegram
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						unknownUser = "unknown"
 | 
				
			||||||
 | 
						HTMLFormat  = "HTML"
 | 
				
			||||||
 | 
						HTMLNick    = "htmlnick"
 | 
				
			||||||
 | 
						MarkdownV2  = "MarkdownV2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Btelegram struct {
 | 
				
			||||||
 | 
						c *tgbotapi.BotAPI
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
						avatarMap map[string]string // keep cache of userid and avatar sha
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						tgsConvertFormat := cfg.GetString("MediaConvertTgs")
 | 
				
			||||||
 | 
						if tgsConvertFormat != "" {
 | 
				
			||||||
 | 
							err := helper.CanConvertTgsToX()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !helper.SupportsFormat(tgsConvertFormat) {
 | 
				
			||||||
 | 
								log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) Connect() error {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						b.Log.Info("Connecting")
 | 
				
			||||||
 | 
						b.c, err = tgbotapi.NewBotAPI(b.GetString("Token"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Debugf("%#v", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						u := tgbotapi.NewUpdate(0)
 | 
				
			||||||
 | 
						u.Timeout = 60
 | 
				
			||||||
 | 
						updates := b.c.GetUpdatesChan(u)
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						go b.handleRecv(updates)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TGGetParseMode(b *Btelegram, username string, text string) (textout string, parsemode string) {
 | 
				
			||||||
 | 
						textout = username + text
 | 
				
			||||||
 | 
						if b.GetString("MessageFormat") == HTMLFormat {
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode HTML")
 | 
				
			||||||
 | 
							parsemode = tgbotapi.ModeHTML
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString("MessageFormat") == "Markdown" {
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode markdown")
 | 
				
			||||||
 | 
							parsemode = tgbotapi.ModeMarkdown
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b.GetString("MessageFormat") == MarkdownV2 {
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode MarkdownV2")
 | 
				
			||||||
 | 
							parsemode = MarkdownV2
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
 | 
				
			||||||
 | 
							b.Log.Debug("Using mode HTML - nick only")
 | 
				
			||||||
 | 
							textout = username + html.EscapeString(text)
 | 
				
			||||||
 | 
							parsemode = tgbotapi.ModeHTML
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return textout, parsemode
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get the chatid
 | 
				
			||||||
 | 
						chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// map the file SHA to our user (caches the avatar)
 | 
				
			||||||
 | 
						if msg.Event == config.EventAvatarDownload {
 | 
				
			||||||
 | 
							return b.cacheAvatar(&msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetString("MessageFormat") == HTMLFormat {
 | 
				
			||||||
 | 
							msg.Text = makeHTML(msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							return b.handleDelete(&msg, chatid)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle prefix hint for unthreaded messages.
 | 
				
			||||||
 | 
						if msg.ParentNotFound() {
 | 
				
			||||||
 | 
							msg.ParentID = ""
 | 
				
			||||||
 | 
							msg.Text = fmt.Sprintf("[reply]: %s", msg.Text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var parentID int
 | 
				
			||||||
 | 
						if msg.ParentID != "" {
 | 
				
			||||||
 | 
							parentID, _ = b.intParentID(msg.ParentID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file if it exists
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text, parentID); msgErr != nil {
 | 
				
			||||||
 | 
									b.Log.Errorf("sendMessage failed: %s", msgErr)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// check if we have files to upload (from slack, telegram or mattermost)
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return b.handleUploadFile(&msg, chatid, parentID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// edit the message if we have a msg ID
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							return b.handleEdit(&msg, chatid)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post normal message
 | 
				
			||||||
 | 
						// TODO: recheck it.
 | 
				
			||||||
 | 
						// Ignore empty text field needs for prevent double messages from whatsapp to telegram
 | 
				
			||||||
 | 
						// when sending media with text caption
 | 
				
			||||||
 | 
						if msg.Text != "" {
 | 
				
			||||||
 | 
							return b.sendMessage(chatid, msg.Username, msg.Text, parentID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) getFileDirectURL(id string) string {
 | 
				
			||||||
 | 
						res, err := b.c.GetFileDirectURL(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return res
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) sendMessage(chatid int64, username, text string, parentID int) (string, error) {
 | 
				
			||||||
 | 
						m := tgbotapi.NewMessage(chatid, "")
 | 
				
			||||||
 | 
						m.Text, m.ParseMode = TGGetParseMode(b, username, text)
 | 
				
			||||||
 | 
						m.ReplyToMessageID = parentID
 | 
				
			||||||
 | 
						m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res, err := b.c.Send(m)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return strconv.Itoa(res.MessageID), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// sendMediaFiles native upload media files via media group
 | 
				
			||||||
 | 
					func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, parentID int, media []interface{}) (string, error) {
 | 
				
			||||||
 | 
						if len(media) == 0 {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mg := tgbotapi.MediaGroupConfig{ChatID: chatid, ChannelUsername: msg.Username, Media: media, ReplyToMessageID: parentID}
 | 
				
			||||||
 | 
						messages, err := b.c.SendMediaGroup(mg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// return first message id
 | 
				
			||||||
 | 
						return strconv.Itoa(messages[0].MessageID), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// intParentID return integer parent id for telegram message
 | 
				
			||||||
 | 
					func (b *Btelegram) intParentID(parentID string) (int, error) {
 | 
				
			||||||
 | 
						pid, err := strconv.Atoi(parentID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return pid, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
 | 
				
			||||||
 | 
						fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
						/* if we have a sha we have successfully uploaded the file to the media server,
 | 
				
			||||||
 | 
						so we can now cache the sha */
 | 
				
			||||||
 | 
						if fi.SHA != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
 | 
				
			||||||
 | 
							b.avatarMap[msg.UserID] = fi.SHA
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										333
									
								
								bridge/vk/vk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								bridge/vk/vk.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,333 @@
 | 
				
			|||||||
 | 
					package bvk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/SevereCloud/vksdk/v2/api"
 | 
				
			||||||
 | 
						"github.com/SevereCloud/vksdk/v2/events"
 | 
				
			||||||
 | 
						longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot"
 | 
				
			||||||
 | 
						"github.com/SevereCloud/vksdk/v2/object"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						audioMessage = "audio_message"
 | 
				
			||||||
 | 
						document     = "doc"
 | 
				
			||||||
 | 
						photo        = "photo"
 | 
				
			||||||
 | 
						video        = "video"
 | 
				
			||||||
 | 
						graffiti     = "graffiti"
 | 
				
			||||||
 | 
						sticker      = "sticker"
 | 
				
			||||||
 | 
						wall         = "wall"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type user struct {
 | 
				
			||||||
 | 
						lastname, firstname, avatar string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bvk struct {
 | 
				
			||||||
 | 
						c            *api.VK
 | 
				
			||||||
 | 
						lp           *longpoll.LongPoll
 | 
				
			||||||
 | 
						usernamesMap map[int]user // cache of user names and avatar URLs
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						return &Bvk{usernamesMap: make(map[int]user), Config: cfg}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) Connect() error {
 | 
				
			||||||
 | 
						b.Log.Info("Connecting")
 | 
				
			||||||
 | 
						b.c = api.NewVK(b.GetString("Token"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						b.lp, err = longpoll.NewLongPollCommunity(b.c)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Debugf("%#v", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
 | 
				
			||||||
 | 
							b.handleMessage(obj.Message, false)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							err := b.lp.Run()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.WithError(err).Fatal("Enable longpoll in group management")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) Disconnect() error {
 | 
				
			||||||
 | 
						b.lp.Shutdown()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peerID, err := strconv.Atoi(msg.Channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params := api.Params{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						text := msg.Username + msg.Text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								// generate attachments string
 | 
				
			||||||
 | 
								attachment, urls := b.uploadFiles(msg.Extra, peerID)
 | 
				
			||||||
 | 
								params["attachment"] = attachment
 | 
				
			||||||
 | 
								text += urls
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params["message"] = text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.ID == "" {
 | 
				
			||||||
 | 
							// New message
 | 
				
			||||||
 | 
							params["random_id"] = time.Now().Unix()
 | 
				
			||||||
 | 
							params["peer_ids"] = msg.Channel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							res, e := b.c.MessagesSendPeerIDs(params)
 | 
				
			||||||
 | 
							if e != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return strconv.Itoa(res[0].ConversationMessageID), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Edit message
 | 
				
			||||||
 | 
						messageID, err := strconv.ParseInt(msg.ID, 10, 64)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params["peer_id"] = peerID
 | 
				
			||||||
 | 
						params["conversation_message_id"] = messageID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = b.c.MessagesEdit(params)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return msg.ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) getUser(id int) user {
 | 
				
			||||||
 | 
						u, found := b.usernamesMap[id]
 | 
				
			||||||
 | 
						if !found {
 | 
				
			||||||
 | 
							b.Log.Debug("Fetching username for ", id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if id >= 0 {
 | 
				
			||||||
 | 
								result, _ := b.c.UsersGet(api.Params{
 | 
				
			||||||
 | 
									"user_ids": id,
 | 
				
			||||||
 | 
									"fields":   "photo_200",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								resUser := result[0]
 | 
				
			||||||
 | 
								u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200}
 | 
				
			||||||
 | 
								b.usernamesMap[id] = u
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								result, _ := b.c.GroupsGetByID(api.Params{
 | 
				
			||||||
 | 
									"group_id": id * -1,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								resGroup := result[0]
 | 
				
			||||||
 | 
								u = user{lastname: resGroup.Name, avatar: resGroup.Photo200}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return u
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) {
 | 
				
			||||||
 | 
						b.Log.Debug("ChatID: ", msg.PeerID)
 | 
				
			||||||
 | 
						// fetch user info
 | 
				
			||||||
 | 
						u := b.getUser(msg.FromID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							Text:     msg.Text,
 | 
				
			||||||
 | 
							Username: u.firstname + " " + u.lastname,
 | 
				
			||||||
 | 
							Avatar:   u.avatar,
 | 
				
			||||||
 | 
							Channel:  strconv.Itoa(msg.PeerID),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							UserID:   strconv.Itoa(msg.FromID),
 | 
				
			||||||
 | 
							ID:       strconv.Itoa(msg.ConversationMessageID),
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.ReplyMessage != nil {
 | 
				
			||||||
 | 
							ur := b.getUser(msg.ReplyMessage.FromID)
 | 
				
			||||||
 | 
							rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if isFwd {
 | 
				
			||||||
 | 
							rmsg.Username = "Fwd: " + rmsg.Username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(msg.Attachments) > 0 {
 | 
				
			||||||
 | 
							urls, text := b.getFiles(msg.Attachments)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if text != "" {
 | 
				
			||||||
 | 
								rmsg.Text += "\n" + text
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// download
 | 
				
			||||||
 | 
							b.downloadFiles(&rmsg, urls)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(msg.FwdMessages) > 0 {
 | 
				
			||||||
 | 
							rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(msg.FwdMessages) > 0 {
 | 
				
			||||||
 | 
							// recursive processing of forwarded messages
 | 
				
			||||||
 | 
							for _, m := range msg.FwdMessages {
 | 
				
			||||||
 | 
								m.PeerID = msg.PeerID
 | 
				
			||||||
 | 
								b.handleMessage(m, true)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) {
 | 
				
			||||||
 | 
						var attachments []string
 | 
				
			||||||
 | 
						text := ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, f := range extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								text += fi.Comment + "\n"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							a, err := b.uploadFile(fi, peerID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.WithError(err).Error("File upload error ", fi.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							attachments = append(attachments, a)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return strings.Join(attachments, ","), text
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) {
 | 
				
			||||||
 | 
						r := bytes.NewReader(*file.Data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						photoRE := regexp.MustCompile(".(jpg|jpe|png)$")
 | 
				
			||||||
 | 
						if photoRE.MatchString(file.Name) {
 | 
				
			||||||
 | 
							// BUG(VK): for community chat peerID=0
 | 
				
			||||||
 | 
							p, err := b.c.UploadMessagesPhoto(0, r)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var doctype string
 | 
				
			||||||
 | 
						if strings.Contains(file.Name, ".ogg") {
 | 
				
			||||||
 | 
							doctype = audioMessage
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							doctype = document
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch doc.Type {
 | 
				
			||||||
 | 
						case audioMessage:
 | 
				
			||||||
 | 
							return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil
 | 
				
			||||||
 | 
						case document:
 | 
				
			||||||
 | 
							return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) {
 | 
				
			||||||
 | 
						var urls []string
 | 
				
			||||||
 | 
						var text []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, a := range attachments {
 | 
				
			||||||
 | 
							switch a.Type {
 | 
				
			||||||
 | 
							case photo:
 | 
				
			||||||
 | 
								var resolution float64 = 0
 | 
				
			||||||
 | 
								url := a.Photo.Sizes[0].URL
 | 
				
			||||||
 | 
								for _, size := range a.Photo.Sizes {
 | 
				
			||||||
 | 
									r := size.Height * size.Width
 | 
				
			||||||
 | 
									if resolution < r {
 | 
				
			||||||
 | 
										resolution = r
 | 
				
			||||||
 | 
										url = size.URL
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								urls = append(urls, url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case document:
 | 
				
			||||||
 | 
								urls = append(urls, a.Doc.URL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case graffiti:
 | 
				
			||||||
 | 
								urls = append(urls, a.Graffiti.URL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case audioMessage:
 | 
				
			||||||
 | 
								urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case sticker:
 | 
				
			||||||
 | 
								var resolution float64 = 0
 | 
				
			||||||
 | 
								url := a.Sticker.Images[0].URL
 | 
				
			||||||
 | 
								for _, size := range a.Sticker.Images {
 | 
				
			||||||
 | 
									r := size.Height * size.Width
 | 
				
			||||||
 | 
									if resolution < r {
 | 
				
			||||||
 | 
										resolution = r
 | 
				
			||||||
 | 
										url = size.URL
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								urls = append(urls, url+".png")
 | 
				
			||||||
 | 
							case video:
 | 
				
			||||||
 | 
								text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case wall:
 | 
				
			||||||
 | 
								text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								text = append(text, "This attachment is not supported ("+a.Type+")")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return urls, strings.Join(text, "\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) {
 | 
				
			||||||
 | 
						for _, url := range urls {
 | 
				
			||||||
 | 
							data, err := helper.DownloadFile(url)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								urlPart := strings.Split(url, "/")
 | 
				
			||||||
 | 
								name := strings.Split(urlPart[len(urlPart)-1], "?")[0]
 | 
				
			||||||
 | 
								helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										382
									
								
								bridge/whatsapp/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								bridge/whatsapp/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,382 @@
 | 
				
			|||||||
 | 
					// nolint:goconst
 | 
				
			||||||
 | 
					package bwhatsapp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/Rhymen/go-whatsapp"
 | 
				
			||||||
 | 
						"github.com/jpillora/backoff"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					Implement handling messages coming from WhatsApp
 | 
				
			||||||
 | 
					Check:
 | 
				
			||||||
 | 
					- https://github.com/Rhymen/go-whatsapp#add-message-handlers
 | 
				
			||||||
 | 
					- https://github.com/Rhymen/go-whatsapp/blob/master/handler.go
 | 
				
			||||||
 | 
					- https://github.com/tulir/mautrix-whatsapp/tree/master/whatsapp-ext for more advanced command handling
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleError received from WhatsApp
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) HandleError(err error) {
 | 
				
			||||||
 | 
						// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
 | 
				
			||||||
 | 
						// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
 | 
				
			||||||
 | 
						if strings.Contains(err.Error(), "error processing data: received invalid data") ||
 | 
				
			||||||
 | 
							strings.Contains(err.Error(), "invalid string with tag 174") {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch err.(type) {
 | 
				
			||||||
 | 
						case *whatsapp.ErrConnectionClosed, *whatsapp.ErrConnectionFailed:
 | 
				
			||||||
 | 
							b.reconnect(err)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							switch err {
 | 
				
			||||||
 | 
							case whatsapp.ErrConnectionTimeout:
 | 
				
			||||||
 | 
								b.reconnect(err)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								b.Log.Errorf("%v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) reconnect(err error) {
 | 
				
			||||||
 | 
						bf := &backoff.Backoff{
 | 
				
			||||||
 | 
							Min:    time.Second,
 | 
				
			||||||
 | 
							Max:    5 * time.Minute,
 | 
				
			||||||
 | 
							Jitter: true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							d := bf.Duration()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Errorf("Connection failed, underlying error: %v", err)
 | 
				
			||||||
 | 
							b.Log.Infof("Waiting %s...", d)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							time.Sleep(d)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Info("Reconnecting...")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := b.conn.Restore()
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								bf.Reset()
 | 
				
			||||||
 | 
								b.startedAt = uint64(time.Now().Unix())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleTextMessage sent from WhatsApp, relay it to the brige
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
 | 
				
			||||||
 | 
						if message.Info.FromMe {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// whatsapp sends last messages to show context , cut them
 | 
				
			||||||
 | 
						if message.Info.Timestamp < b.startedAt {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						groupJID := message.Info.RemoteJid
 | 
				
			||||||
 | 
						senderJID := message.Info.SenderJid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(senderJID) == 0 {
 | 
				
			||||||
 | 
							if message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
				
			||||||
 | 
								senderJID = *message.Info.Source.Participant
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// translate sender's JID to the nicest username we can get
 | 
				
			||||||
 | 
						senderName := b.getSenderName(senderJID)
 | 
				
			||||||
 | 
						if senderName == "" {
 | 
				
			||||||
 | 
							senderName = "Someone" // don't expose telephone number
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						extText := message.Info.Source.Message.ExtendedTextMessage
 | 
				
			||||||
 | 
						if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
 | 
				
			||||||
 | 
							// handle user mentions
 | 
				
			||||||
 | 
							for _, mentionedJID := range extText.ContextInfo.MentionedJid {
 | 
				
			||||||
 | 
								numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// mentions comes as telephone numbers and we don't want to expose it to other bridges
 | 
				
			||||||
 | 
								// replace it with something more meaninful to others
 | 
				
			||||||
 | 
								mention := b.getSenderNotify(numberAndSuffix[0] + "@s.whatsapp.net")
 | 
				
			||||||
 | 
								if mention == "" {
 | 
				
			||||||
 | 
									mention = "someone"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID,
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Text:     message.Text,
 | 
				
			||||||
 | 
							Channel:  groupJID,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							//	ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string
 | 
				
			||||||
 | 
							ID: message.Info.Id,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleImageMessage sent from WhatsApp, relay it to the brige
 | 
				
			||||||
 | 
					// nolint:funlen
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
 | 
				
			||||||
 | 
						if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := message.Info.SenderJid
 | 
				
			||||||
 | 
						if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = *message.Info.Source.Participant
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderName := b.getSenderName(message.Info.SenderJid)
 | 
				
			||||||
 | 
						if senderName == "" {
 | 
				
			||||||
 | 
							senderName = "Someone" // don't expose telephone number
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID,
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  message.Info.RemoteJid,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       message.Info.Id,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(message.Type)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
 | 
				
			||||||
 | 
						if fileExt[0] == ".jfif" {
 | 
				
			||||||
 | 
							fileExt[0] = ".jpg"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
 | 
				
			||||||
 | 
						if fileExt[0] == ".jpe" {
 | 
				
			||||||
 | 
							fileExt[0] = ".jpg"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := message.Download()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download image failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleVideoMessage downloads video messages
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
 | 
				
			||||||
 | 
						if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := message.Info.SenderJid
 | 
				
			||||||
 | 
						if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = *message.Info.Source.Participant
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderName := b.getSenderName(message.Info.SenderJid)
 | 
				
			||||||
 | 
						if senderName == "" {
 | 
				
			||||||
 | 
							senderName = "Someone" // don't expose telephone number
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID,
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  message.Info.RemoteJid,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       message.Info.Id,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(message.Type)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(fileExt) == 0 {
 | 
				
			||||||
 | 
							fileExt = append(fileExt, ".mp4")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := message.Download()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download video failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleAudioMessage downloads audio messages
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
 | 
				
			||||||
 | 
						if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := message.Info.SenderJid
 | 
				
			||||||
 | 
						if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = *message.Info.Source.Participant
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderName := b.getSenderName(message.Info.SenderJid)
 | 
				
			||||||
 | 
						if senderName == "" {
 | 
				
			||||||
 | 
							senderName = "Someone" // don't expose telephone number
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID,
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  message.Info.RemoteJid,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       message.Info.Id,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(message.Type)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(fileExt) == 0 {
 | 
				
			||||||
 | 
							fileExt = append(fileExt, ".ogg")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := message.Download()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download audio failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleDocumentMessage downloads documents
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) {
 | 
				
			||||||
 | 
						if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := message.Info.SenderJid
 | 
				
			||||||
 | 
						if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = *message.Info.Source.Participant
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderName := b.getSenderName(message.Info.SenderJid)
 | 
				
			||||||
 | 
						if senderName == "" {
 | 
				
			||||||
 | 
							senderName = "Someone" // don't expose telephone number
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID,
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  message.Info.RemoteJid,
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       message.Info.Id,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(message.Type)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v", message.FileName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := message.Download()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download document message failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										162
									
								
								bridge/whatsapp/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								bridge/whatsapp/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					package bwhatsapp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
 | 
				
			||||||
 | 
						"github.com/Rhymen/go-whatsapp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProfilePicInfo struct {
 | 
				
			||||||
 | 
						URL    string `json:"eurl"`
 | 
				
			||||||
 | 
						Tag    string `json:"tag"`
 | 
				
			||||||
 | 
						Status int16  `json:"status"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func qrFromTerminal(invert bool) chan string {
 | 
				
			||||||
 | 
						qr := make(chan string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							terminal := qrcodeTerminal.New()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if invert {
 | 
				
			||||||
 | 
								terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							terminal.Get(<-qr).Print()
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return qr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
 | 
				
			||||||
 | 
						session := whatsapp.Session{}
 | 
				
			||||||
 | 
						sessionFile := b.Config.GetString(sessionFile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if sessionFile == "" {
 | 
				
			||||||
 | 
							return session, errors.New("if you won't set SessionFile then you will need to scan QR code on every restart")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						file, err := os.Open(sessionFile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return session, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defer file.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						decoder := gob.NewDecoder(file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return session, decoder.Decode(&session)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
 | 
				
			||||||
 | 
						sessionFile := b.Config.GetString(sessionFile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if sessionFile == "" {
 | 
				
			||||||
 | 
							// we already sent a warning while starting the bridge, so let's be quiet here
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						file, err := os.Create(sessionFile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defer file.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encoder := gob.NewEncoder(file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return encoder.Encode(session)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
 | 
				
			||||||
 | 
						session, err := b.readSession()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Warn(err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugln("Restoring WhatsApp session..")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session, err = b.conn.RestoreWithSession(session)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							// restore session connection timed out (I couldn't get over it without logging in again)
 | 
				
			||||||
 | 
							return nil, errors.New("failed to restore session: " + err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugln("Session restored successfully!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &session, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) getSenderName(senderJid string) string {
 | 
				
			||||||
 | 
						if sender, exists := b.users[senderJid]; exists {
 | 
				
			||||||
 | 
							if sender.Name != "" {
 | 
				
			||||||
 | 
								return sender.Name
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// if user is not in phone contacts
 | 
				
			||||||
 | 
							// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
 | 
				
			||||||
 | 
							// users can change it in their WhatsApp settings -> profile -> click on Avatar
 | 
				
			||||||
 | 
							if sender.Notify != "" {
 | 
				
			||||||
 | 
								return sender.Notify
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if sender.Short != "" {
 | 
				
			||||||
 | 
								return sender.Short
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// try to reload this contact
 | 
				
			||||||
 | 
						if _, err := b.conn.Contacts(); err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("error on update of contacts: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if contact, exists := b.conn.Store.Contacts[senderJid]; exists {
 | 
				
			||||||
 | 
							// Add it to the user map
 | 
				
			||||||
 | 
							b.users[senderJid] = contact
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if contact.Name != "" {
 | 
				
			||||||
 | 
								return contact.Name
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// if user is not in phone contacts
 | 
				
			||||||
 | 
							// same as above
 | 
				
			||||||
 | 
							return contact.Notify
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
 | 
				
			||||||
 | 
						if sender, exists := b.users[senderJid]; exists {
 | 
				
			||||||
 | 
							return sender.Notify
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
 | 
				
			||||||
 | 
						data, err := b.conn.GetProfilePicThumb(jid)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get avatar: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content := <-data
 | 
				
			||||||
 | 
						info := &ProfilePicInfo{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = json.Unmarshal([]byte(content), info)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return info, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isGroupJid(identifier string) bool {
 | 
				
			||||||
 | 
						return strings.HasSuffix(identifier, "@g.us") ||
 | 
				
			||||||
 | 
							strings.HasSuffix(identifier, "@temp") ||
 | 
				
			||||||
 | 
							strings.HasSuffix(identifier, "@broadcast")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										341
									
								
								bridge/whatsapp/whatsapp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								bridge/whatsapp/whatsapp.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,341 @@
 | 
				
			|||||||
 | 
					package bwhatsapp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"encoding/hex"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/Rhymen/go-whatsapp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// Account config parameters
 | 
				
			||||||
 | 
						cfgNumber         = "Number"
 | 
				
			||||||
 | 
						qrOnWhiteTerminal = "QrOnWhiteTerminal"
 | 
				
			||||||
 | 
						sessionFile       = "SessionFile"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Bwhatsapp Bridge structure keeping all the information needed for relying
 | 
				
			||||||
 | 
					type Bwhatsapp struct {
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session   *whatsapp.Session
 | 
				
			||||||
 | 
						conn      *whatsapp.Conn
 | 
				
			||||||
 | 
						startedAt uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						users       map[string]whatsapp.Contact
 | 
				
			||||||
 | 
						userAvatars map[string]string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						number := cfg.GetString(cfgNumber)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
 | 
				
			||||||
 | 
						cfg.Log.Warn("This bridge is deprecated and not supported anymore. Use the new multidevice whatsapp bridge")
 | 
				
			||||||
 | 
						cfg.Log.Warn("See https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support for more info")
 | 
				
			||||||
 | 
						cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if number == "" {
 | 
				
			||||||
 | 
							cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b := &Bwhatsapp{
 | 
				
			||||||
 | 
							Config: cfg,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							users:       make(map[string]whatsapp.Contact),
 | 
				
			||||||
 | 
							userAvatars: make(map[string]string),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Connect to WhatsApp. Required implementation of the Bridger interface
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) Connect() error {
 | 
				
			||||||
 | 
						number := b.GetString(cfgNumber)
 | 
				
			||||||
 | 
						if number == "" {
 | 
				
			||||||
 | 
							return errors.New("whatsapp's telephone number need to be configured")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugln("Connecting to WhatsApp..")
 | 
				
			||||||
 | 
						conn, err := whatsapp.NewConn(20 * time.Second)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return errors.New("failed to connect to WhatsApp: " + err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.conn = conn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.conn.AddHandler(b)
 | 
				
			||||||
 | 
						b.Log.Debugln("WhatsApp connection successful")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// load existing session in order to keep it between restarts
 | 
				
			||||||
 | 
						b.session, err = b.restoreSession()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Warn(err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// login to a new session
 | 
				
			||||||
 | 
						if b.session == nil {
 | 
				
			||||||
 | 
							if err = b.Login(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.startedAt = uint64(time.Now().Unix())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = b.conn.Contacts()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error on update of contacts: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
 | 
				
			||||||
 | 
						for len(b.conn.Store.Contacts) == 0 {
 | 
				
			||||||
 | 
							b.conn.Contacts() // nolint:errcheck
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<-time.After(1 * time.Second)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// map all the users
 | 
				
			||||||
 | 
						for id, contact := range b.conn.Store.Contacts {
 | 
				
			||||||
 | 
							if !isGroupJid(id) && id != "status@broadcast" {
 | 
				
			||||||
 | 
								// it is user
 | 
				
			||||||
 | 
								b.users[id] = contact
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get user avatar asynchronously
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							b.Log.Debug("Getting user avatars..")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for jid := range b.users {
 | 
				
			||||||
 | 
								info, err := b.GetProfilePicThumb(jid)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									b.Lock()
 | 
				
			||||||
 | 
									b.userAvatars[jid] = info.URL
 | 
				
			||||||
 | 
									b.Unlock()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debug("Finished getting avatars..")
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Login to WhatsApp creating a new session. This will require to scan a QR code on your mobile device
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) Login() error {
 | 
				
			||||||
 | 
						b.Log.Debugln("Logging in..")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						invert := b.GetBool(qrOnWhiteTerminal) // false is the default
 | 
				
			||||||
 | 
						qrChan := qrFromTerminal(invert)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session, err := b.conn.Login(qrChan)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Warnln("Failed to log in:", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.session = &session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Infof("Logged into session: %#v", session)
 | 
				
			||||||
 | 
						b.Log.Infof("Connection: %#v", b.conn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = b.writeSession(session)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Disconnect is called while reconnecting to the bridge
 | 
				
			||||||
 | 
					// Required implementation of the Bridger interface
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) Disconnect() error {
 | 
				
			||||||
 | 
						// We could Logout, but that would close the session completely and would require a new QR code scan
 | 
				
			||||||
 | 
						// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
 | 
				
			||||||
 | 
					// Required implementation of the Bridger interface
 | 
				
			||||||
 | 
					// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						byJid := isGroupJid(channel.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
 | 
				
			||||||
 | 
						for len(b.conn.Store.Contacts) == 0 {
 | 
				
			||||||
 | 
							b.conn.Contacts() // nolint:errcheck
 | 
				
			||||||
 | 
							<-time.After(1 * time.Second)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// verify if we are member of the given group
 | 
				
			||||||
 | 
						if byJid {
 | 
				
			||||||
 | 
							// channel.Name specifies static group jID, not the name
 | 
				
			||||||
 | 
							if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
 | 
				
			||||||
 | 
								return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// channel.Name specifies group name that might change, warn about it
 | 
				
			||||||
 | 
						var jids []string
 | 
				
			||||||
 | 
						for id, contact := range b.conn.Store.Contacts {
 | 
				
			||||||
 | 
							if isGroupJid(id) && contact.Name == channel.Name {
 | 
				
			||||||
 | 
								jids = append(jids, id)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch len(jids) {
 | 
				
			||||||
 | 
						case 0:
 | 
				
			||||||
 | 
							// didn't match any group - print out possibilites
 | 
				
			||||||
 | 
							for id, contact := range b.conn.Store.Contacts {
 | 
				
			||||||
 | 
								if isGroupJid(id) {
 | 
				
			||||||
 | 
									b.Log.Infof("%s %s", contact.Jid, contact.Name)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
 | 
				
			||||||
 | 
						case 1:
 | 
				
			||||||
 | 
							return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Post a document message from the bridge to WhatsApp
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
 | 
				
			||||||
 | 
						fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post document message
 | 
				
			||||||
 | 
						message := whatsapp.DocumentMessage{
 | 
				
			||||||
 | 
							Info: whatsapp.MessageInfo{
 | 
				
			||||||
 | 
								RemoteJid: msg.Channel,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Title:    fi.Name,
 | 
				
			||||||
 | 
							FileName: fi.Name,
 | 
				
			||||||
 | 
							Type:     filetype,
 | 
				
			||||||
 | 
							Content:  bytes.NewReader(*fi.Data),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Sending %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create message ID
 | 
				
			||||||
 | 
						// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
 | 
				
			||||||
 | 
						idBytes := make([]byte, 10)
 | 
				
			||||||
 | 
						if _, err := rand.Read(idBytes); err != nil {
 | 
				
			||||||
 | 
							b.Log.Warn(err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
 | 
				
			||||||
 | 
						_, err := b.conn.Send(message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return message.Info.Id, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Post an image message from the bridge to WhatsApp
 | 
				
			||||||
 | 
					// Handle, for sure image/jpeg, image/png and image/gif MIME types
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
 | 
				
			||||||
 | 
						fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post image message
 | 
				
			||||||
 | 
						message := whatsapp.ImageMessage{
 | 
				
			||||||
 | 
							Info: whatsapp.MessageInfo{
 | 
				
			||||||
 | 
								RemoteJid: msg.Channel,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Type:    filetype,
 | 
				
			||||||
 | 
							Caption: msg.Username + fi.Comment,
 | 
				
			||||||
 | 
							Content: bytes.NewReader(*fi.Data),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Sending %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create message ID
 | 
				
			||||||
 | 
						// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
 | 
				
			||||||
 | 
						idBytes := make([]byte, 10)
 | 
				
			||||||
 | 
						if _, err := rand.Read(idBytes); err != nil {
 | 
				
			||||||
 | 
							b.Log.Warn(err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
 | 
				
			||||||
 | 
						_, err := b.conn.Send(message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return message.Info.Id, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Send a message from the bridge to WhatsApp
 | 
				
			||||||
 | 
					// Required implementation of the Bridger interface
 | 
				
			||||||
 | 
					// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								// No message ID in case action is executed on a message sent before the bridge was started
 | 
				
			||||||
 | 
								// and then the bridge cache doesn't have this message ID mapped
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("updating message with id %s", msg.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.GetString("editsuffix") != "" {
 | 
				
			||||||
 | 
								msg.Text += b.GetString("EditSuffix")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								msg.Text += " (edited)"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle Upload a file
 | 
				
			||||||
 | 
						if msg.Extra["file"] != nil {
 | 
				
			||||||
 | 
							fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
							filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("Extra file is %#v", filetype)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO: add different types
 | 
				
			||||||
 | 
							// TODO: add webp conversion
 | 
				
			||||||
 | 
							switch filetype {
 | 
				
			||||||
 | 
							case "image/jpeg", "image/png", "image/gif":
 | 
				
			||||||
 | 
								return b.PostImageMessage(msg, filetype)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return b.PostDocumentMessage(msg, filetype)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post text message
 | 
				
			||||||
 | 
						message := whatsapp.TextMessage{
 | 
				
			||||||
 | 
							Info: whatsapp.MessageInfo{
 | 
				
			||||||
 | 
								RemoteJid: msg.Channel, // which equals to group id
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Text: msg.Username + msg.Text,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Sending %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.conn.Send(message)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
 | 
				
			||||||
 | 
					//func (b *Bwhatsapp) Command(cmd string) string {
 | 
				
			||||||
 | 
					//	return ""
 | 
				
			||||||
 | 
					//}
 | 
				
			||||||
							
								
								
									
										344
									
								
								bridge/whatsappmulti/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								bridge/whatsappmulti/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,344 @@
 | 
				
			|||||||
 | 
					// +build whatsappmulti
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bwhatsapp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/binary/proto"
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/types"
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/types/events"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint:gocritic
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) eventHandler(evt interface{}) {
 | 
				
			||||||
 | 
						switch e := evt.(type) {
 | 
				
			||||||
 | 
						case *events.Message:
 | 
				
			||||||
 | 
							b.handleMessage(e)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) handleMessage(message *events.Message) {
 | 
				
			||||||
 | 
						msg := message.Message
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt):
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Infof("Receiving message %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case msg.Conversation != nil || msg.ExtendedTextMessage != nil:
 | 
				
			||||||
 | 
							b.handleTextMessage(message.Info, msg)
 | 
				
			||||||
 | 
						case msg.VideoMessage != nil:
 | 
				
			||||||
 | 
							b.handleVideoMessage(message)
 | 
				
			||||||
 | 
						case msg.AudioMessage != nil:
 | 
				
			||||||
 | 
							b.handleAudioMessage(message)
 | 
				
			||||||
 | 
						case msg.DocumentMessage != nil:
 | 
				
			||||||
 | 
							b.handleDocumentMessage(message)
 | 
				
			||||||
 | 
						case msg.ImageMessage != nil:
 | 
				
			||||||
 | 
							b.handleImageMessage(message)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint:funlen
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) {
 | 
				
			||||||
 | 
						senderJID := messageInfo.Sender
 | 
				
			||||||
 | 
						channel := messageInfo.Chat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderName := b.getSenderName(messageInfo.Sender)
 | 
				
			||||||
 | 
						if senderName == "" {
 | 
				
			||||||
 | 
							senderName = "Someone" // don't expose telephone number
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.GetExtendedTextMessage() == nil && msg.GetConversation() == "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("message without text content? %#v", msg)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var text string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// nolint:nestif
 | 
				
			||||||
 | 
						if msg.GetExtendedTextMessage() == nil {
 | 
				
			||||||
 | 
							text = msg.GetConversation()
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							text = msg.GetExtendedTextMessage().GetText()
 | 
				
			||||||
 | 
							ci := msg.GetExtendedTextMessage().GetContextInfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
				
			||||||
 | 
								senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ci.MentionedJid != nil {
 | 
				
			||||||
 | 
								// handle user mentions
 | 
				
			||||||
 | 
								for _, mentionedJID := range ci.MentionedJid {
 | 
				
			||||||
 | 
									numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// mentions comes as telephone numbers and we don't want to expose it to other bridges
 | 
				
			||||||
 | 
									// replace it with something more meaninful to others
 | 
				
			||||||
 | 
									mention := b.getSenderNotify(types.NewJID(numberAndSuffix[0], types.DefaultUserServer))
 | 
				
			||||||
 | 
									if mention == "" {
 | 
				
			||||||
 | 
										mention = "someone"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID.String(),
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Text:     text,
 | 
				
			||||||
 | 
							Channel:  channel.String(),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							//      ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string
 | 
				
			||||||
 | 
							ID: messageInfo.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleImageMessage sent from WhatsApp, relay it to the brige
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
 | 
				
			||||||
 | 
						imsg := msg.Message.GetImageMessage()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := msg.Info.Sender
 | 
				
			||||||
 | 
						senderName := b.getSenderName(senderJID)
 | 
				
			||||||
 | 
						ci := imsg.GetContextInfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID.String(),
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  msg.Info.Chat.String(),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       msg.Info.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
 | 
				
			||||||
 | 
						if fileExt[0] == ".jfif" {
 | 
				
			||||||
 | 
							fileExt[0] = ".jpg"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
 | 
				
			||||||
 | 
						if fileExt[0] == ".jpe" {
 | 
				
			||||||
 | 
							fileExt[0] = ".jpg"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := b.wc.Download(imsg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download image failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleVideoMessage downloads video messages
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
 | 
				
			||||||
 | 
						imsg := msg.Message.GetVideoMessage()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := msg.Info.Sender
 | 
				
			||||||
 | 
						senderName := b.getSenderName(senderJID)
 | 
				
			||||||
 | 
						ci := imsg.GetContextInfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID.String(),
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  msg.Info.Chat.String(),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       msg.Info.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(fileExt) == 0 {
 | 
				
			||||||
 | 
							fileExt = append(fileExt, ".mp4")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := b.wc.Download(imsg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download video failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleAudioMessage downloads audio messages
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
 | 
				
			||||||
 | 
						imsg := msg.Message.GetAudioMessage()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := msg.Info.Sender
 | 
				
			||||||
 | 
						senderName := b.getSenderName(senderJID)
 | 
				
			||||||
 | 
						ci := imsg.GetContextInfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID.String(),
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  msg.Info.Chat.String(),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       msg.Info.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(fileExt) == 0 {
 | 
				
			||||||
 | 
							fileExt = append(fileExt, ".ogg")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := b.wc.Download(imsg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download video failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleDocumentMessage downloads documents
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
 | 
				
			||||||
 | 
						imsg := msg.Message.GetDocumentMessage()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						senderJID := msg.Info.Sender
 | 
				
			||||||
 | 
						senderName := b.getSenderName(senderJID)
 | 
				
			||||||
 | 
						ci := imsg.GetContextInfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
				
			||||||
 | 
							senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							UserID:   senderJID.String(),
 | 
				
			||||||
 | 
							Username: senderName,
 | 
				
			||||||
 | 
							Channel:  msg.Info.Chat.String(),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							Protocol: b.Protocol,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
							ID:       msg.Info.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
				
			||||||
 | 
							rmsg.Avatar = avatarURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Mimetype detection error: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%v", imsg.GetFileName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data, err := b.wc.Download(imsg)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Download document message failed: %s", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Move file to bridge storage
 | 
				
			||||||
 | 
						helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
				
			||||||
 | 
						b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Remote <- rmsg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										108
									
								
								bridge/whatsappmulti/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								bridge/whatsappmulti/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					// +build whatsappmulti
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bwhatsapp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/store"
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/store/sqlstore"
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProfilePicInfo struct {
 | 
				
			||||||
 | 
						URL    string `json:"eurl"`
 | 
				
			||||||
 | 
						Tag    string `json:"tag"`
 | 
				
			||||||
 | 
						Status int16  `json:"status"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) getSenderName(senderJid types.JID) string {
 | 
				
			||||||
 | 
						if sender, exists := b.contacts[senderJid]; exists {
 | 
				
			||||||
 | 
							if sender.FullName != "" {
 | 
				
			||||||
 | 
								return sender.FullName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// if user is not in phone contacts
 | 
				
			||||||
 | 
							// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
 | 
				
			||||||
 | 
							// users can change it in their WhatsApp settings -> profile -> click on Avatar
 | 
				
			||||||
 | 
							if sender.PushName != "" {
 | 
				
			||||||
 | 
								return sender.PushName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if sender.FirstName != "" {
 | 
				
			||||||
 | 
								return sender.FirstName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// try to reload this contact
 | 
				
			||||||
 | 
						if _, err := b.wc.Store.Contacts.GetAllContacts(); err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("error on update of contacts: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allcontacts, err := b.wc.Store.Contacts.GetAllContacts()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("error on update of contacts: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(allcontacts) > 0 {
 | 
				
			||||||
 | 
							b.contacts = allcontacts
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if sender, exists := b.contacts[senderJid]; exists {
 | 
				
			||||||
 | 
							if sender.FullName != "" {
 | 
				
			||||||
 | 
								return sender.FullName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// if user is not in phone contacts
 | 
				
			||||||
 | 
							// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
 | 
				
			||||||
 | 
							// users can change it in their WhatsApp settings -> profile -> click on Avatar
 | 
				
			||||||
 | 
							if sender.PushName != "" {
 | 
				
			||||||
 | 
								return sender.PushName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if sender.FirstName != "" {
 | 
				
			||||||
 | 
								return sender.FirstName
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "Someone"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string {
 | 
				
			||||||
 | 
						if sender, exists := b.contacts[senderJid]; exists {
 | 
				
			||||||
 | 
							return sender.PushName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*types.ProfilePictureInfo, error) {
 | 
				
			||||||
 | 
						pjid, _ := types.ParseJID(jid)
 | 
				
			||||||
 | 
						info, err := b.wc.GetProfilePictureInfo(pjid, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get avatar: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return info, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isGroupJid(identifier string) bool {
 | 
				
			||||||
 | 
						return strings.HasSuffix(identifier, "@g.us") ||
 | 
				
			||||||
 | 
							strings.HasSuffix(identifier, "@temp") ||
 | 
				
			||||||
 | 
							strings.HasSuffix(identifier, "@broadcast")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) getDevice() (*store.Device, error) {
 | 
				
			||||||
 | 
						device := &store.Device{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_foreign_keys=on&_pragma=busy_timeout=10000", nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return device, fmt.Errorf("failed to connect to database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						device, err = storeContainer.GetFirstDevice()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return device, fmt.Errorf("failed to get device: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return device, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										333
									
								
								bridge/whatsappmulti/whatsapp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								bridge/whatsappmulti/whatsapp.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,333 @@
 | 
				
			|||||||
 | 
					// +build whatsappmulti
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bwhatsapp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/mdp/qrterminal"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow"
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/binary/proto"
 | 
				
			||||||
 | 
						"go.mau.fi/whatsmeow/types"
 | 
				
			||||||
 | 
						waLog "go.mau.fi/whatsmeow/util/log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						goproto "google.golang.org/protobuf/proto"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_ "modernc.org/sqlite" // needed for sqlite
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// Account config parameters
 | 
				
			||||||
 | 
						cfgNumber = "Number"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Bwhatsapp Bridge structure keeping all the information needed for relying
 | 
				
			||||||
 | 
					type Bwhatsapp struct {
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						startedAt   time.Time
 | 
				
			||||||
 | 
						wc          *whatsmeow.Client
 | 
				
			||||||
 | 
						contacts    map[types.JID]types.ContactInfo
 | 
				
			||||||
 | 
						users       map[string]types.ContactInfo
 | 
				
			||||||
 | 
						userAvatars map[string]string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						number := cfg.GetString(cfgNumber)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if number == "" {
 | 
				
			||||||
 | 
							cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b := &Bwhatsapp{
 | 
				
			||||||
 | 
							Config: cfg,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							users:       make(map[string]types.ContactInfo),
 | 
				
			||||||
 | 
							userAvatars: make(map[string]string),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Connect to WhatsApp. Required implementation of the Bridger interface
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) Connect() error {
 | 
				
			||||||
 | 
						device, err := b.getDevice()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						number := b.GetString(cfgNumber)
 | 
				
			||||||
 | 
						if number == "" {
 | 
				
			||||||
 | 
							return errors.New("whatsapp's telephone number need to be configured")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugln("Connecting to WhatsApp..")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true))
 | 
				
			||||||
 | 
						b.wc.AddEventHandler(b.eventHandler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						firstlogin := false
 | 
				
			||||||
 | 
						var qrChan <-chan whatsmeow.QRChannelItem
 | 
				
			||||||
 | 
						if b.wc.Store.ID == nil {
 | 
				
			||||||
 | 
							firstlogin = true
 | 
				
			||||||
 | 
							qrChan, err = b.wc.GetQRChannel(context.Background())
 | 
				
			||||||
 | 
							if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
 | 
				
			||||||
 | 
								return errors.New("failed to to get QR channel:" + err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = b.wc.Connect()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return errors.New("failed to connect to WhatsApp: " + err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.wc.Store.ID == nil {
 | 
				
			||||||
 | 
							for evt := range qrChan {
 | 
				
			||||||
 | 
								if evt.Event == "code" {
 | 
				
			||||||
 | 
									qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									b.Log.Infof("QR channel result: %s", evt.Event)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// disconnect and reconnect on our first login/pairing
 | 
				
			||||||
 | 
						// for some reason the GetJoinedGroups in JoinChannel doesn't work on first login
 | 
				
			||||||
 | 
						if firstlogin {
 | 
				
			||||||
 | 
							b.wc.Disconnect()
 | 
				
			||||||
 | 
							time.Sleep(time.Second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = b.wc.Connect()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return errors.New("failed to connect to WhatsApp: " + err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Infoln("WhatsApp connection successful")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.contacts, err = b.wc.Store.Contacts.GetAllContacts()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return errors.New("failed to get contacts: " + err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.startedAt = time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// map all the users
 | 
				
			||||||
 | 
						for id, contact := range b.contacts {
 | 
				
			||||||
 | 
							if !isGroupJid(id.String()) && id.String() != "status@broadcast" {
 | 
				
			||||||
 | 
								// it is user
 | 
				
			||||||
 | 
								b.users[id.String()] = contact
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get user avatar asynchronously
 | 
				
			||||||
 | 
						b.Log.Info("Getting user avatars..")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for jid := range b.users {
 | 
				
			||||||
 | 
							info, err := b.GetProfilePicThumb(jid)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								b.Lock()
 | 
				
			||||||
 | 
								if info != nil {
 | 
				
			||||||
 | 
									b.userAvatars[jid] = info.URL
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.Unlock()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Info("Finished getting avatars..")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Disconnect is called while reconnecting to the bridge
 | 
				
			||||||
 | 
					// Required implementation of the Bridger interface
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) Disconnect() error {
 | 
				
			||||||
 | 
						b.wc.Disconnect()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
 | 
				
			||||||
 | 
					// Required implementation of the Bridger interface
 | 
				
			||||||
 | 
					// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						byJid := isGroupJid(channel.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						groups, err := b.wc.GetJoinedGroups()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// verify if we are member of the given group
 | 
				
			||||||
 | 
						if byJid {
 | 
				
			||||||
 | 
							gJID, err := types.ParseJID(channel.Name)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, group := range groups {
 | 
				
			||||||
 | 
								if group.JID == gJID {
 | 
				
			||||||
 | 
									return nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						foundGroups := []string{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, group := range groups {
 | 
				
			||||||
 | 
							if group.Name == channel.Name {
 | 
				
			||||||
 | 
								foundGroups = append(foundGroups, group.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch len(foundGroups) {
 | 
				
			||||||
 | 
						case 0:
 | 
				
			||||||
 | 
							// didn't match any group - print out possibilites
 | 
				
			||||||
 | 
							for _, group := range groups {
 | 
				
			||||||
 | 
								b.Log.Infof("%s %s", group.JID, group.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
 | 
				
			||||||
 | 
						case 1:
 | 
				
			||||||
 | 
							return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", foundGroups[0], channel.Name)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, foundGroups)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Post a document message from the bridge to WhatsApp
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
 | 
				
			||||||
 | 
						groupJID, _ := types.ParseJID(msg.Channel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post document message
 | 
				
			||||||
 | 
						var message proto.Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						message.DocumentMessage = &proto.DocumentMessage{
 | 
				
			||||||
 | 
							Title:         &fi.Name,
 | 
				
			||||||
 | 
							FileName:      &fi.Name,
 | 
				
			||||||
 | 
							Mimetype:      &filetype,
 | 
				
			||||||
 | 
							MediaKey:      resp.MediaKey,
 | 
				
			||||||
 | 
							FileEncSha256: resp.FileEncSHA256,
 | 
				
			||||||
 | 
							FileSha256:    resp.FileSHA256,
 | 
				
			||||||
 | 
							FileLength:    goproto.Uint64(resp.FileLength),
 | 
				
			||||||
 | 
							Url:           &resp.URL,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Sending %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ID := whatsmeow.GenerateMessageID()
 | 
				
			||||||
 | 
						_, err = b.wc.SendMessage(groupJID, ID, &message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ID, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Post an image message from the bridge to WhatsApp
 | 
				
			||||||
 | 
					// Handle, for sure image/jpeg, image/png and image/gif MIME types
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
 | 
				
			||||||
 | 
						groupJID, _ := types.ParseJID(msg.Channel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						caption := msg.Username + fi.Comment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var message proto.Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						message.ImageMessage = &proto.ImageMessage{
 | 
				
			||||||
 | 
							Mimetype:      &filetype,
 | 
				
			||||||
 | 
							Caption:       &caption,
 | 
				
			||||||
 | 
							MediaKey:      resp.MediaKey,
 | 
				
			||||||
 | 
							FileEncSha256: resp.FileEncSHA256,
 | 
				
			||||||
 | 
							FileSha256:    resp.FileSHA256,
 | 
				
			||||||
 | 
							FileLength:    goproto.Uint64(resp.FileLength),
 | 
				
			||||||
 | 
							Url:           &resp.URL,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Sending %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ID := whatsmeow.GenerateMessageID()
 | 
				
			||||||
 | 
						_, err = b.wc.SendMessage(groupJID, ID, &message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ID, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Send a message from the bridge to WhatsApp
 | 
				
			||||||
 | 
					func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						groupJID, _ := types.ParseJID(msg.Channel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								// No message ID in case action is executed on a message sent before the bridge was started
 | 
				
			||||||
 | 
								// and then the bridge cache doesn't have this message ID mapped
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err := b.wc.RevokeMessage(groupJID, msg.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Edit message
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("updating message with id %s", msg.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if b.GetString("editsuffix") != "" {
 | 
				
			||||||
 | 
								msg.Text += b.GetString("EditSuffix")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								msg.Text += " (edited)"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Handle Upload a file
 | 
				
			||||||
 | 
						if msg.Extra["file"] != nil {
 | 
				
			||||||
 | 
							fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
							filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b.Log.Debugf("Extra file is %#v", filetype)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO: add different types
 | 
				
			||||||
 | 
							// TODO: add webp conversion
 | 
				
			||||||
 | 
							switch filetype {
 | 
				
			||||||
 | 
							case "image/jpeg", "image/png", "image/gif":
 | 
				
			||||||
 | 
								return b.PostImageMessage(msg, filetype)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return b.PostDocumentMessage(msg, filetype)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						text := msg.Username + msg.Text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var message proto.Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						message.Conversation = &text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ID := whatsmeow.GenerateMessageID()
 | 
				
			||||||
 | 
						_, err := b.wc.SendMessage(groupJID, ID, &message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ID, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										34
									
								
								bridge/xmpp/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								bridge/xmpp/handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					package bxmpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/matterbridge/go-xmpp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleDownloadAvatar downloads the avatar of userid from channel
 | 
				
			||||||
 | 
					// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
 | 
				
			||||||
 | 
					// logs an error message if it fails
 | 
				
			||||||
 | 
					func (b *Bxmpp) handleDownloadAvatar(avatar xmpp.AvatarData) {
 | 
				
			||||||
 | 
						rmsg := config.Message{
 | 
				
			||||||
 | 
							Username: "system",
 | 
				
			||||||
 | 
							Text:     "avatar",
 | 
				
			||||||
 | 
							Channel:  b.parseChannel(avatar.From),
 | 
				
			||||||
 | 
							Account:  b.Account,
 | 
				
			||||||
 | 
							UserID:   avatar.From,
 | 
				
			||||||
 | 
							Event:    config.EventAvatarDownload,
 | 
				
			||||||
 | 
							Extra:    make(map[string][]interface{}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, ok := b.avatarMap[avatar.From]; !ok {
 | 
				
			||||||
 | 
							b.Log.Debugf("Avatar.From: %s", avatar.From)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := helper.HandleDownloadSize(b.Log, &rmsg, avatar.From+".png", int64(len(avatar.Data)), b.General)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Error(err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							helper.HandleDownloadData(b.Log, &rmsg, avatar.From+".png", rmsg.Text, "", &avatar.Data, b.General)
 | 
				
			||||||
 | 
							b.Log.Debugf("Avatar download complete")
 | 
				
			||||||
 | 
							b.Remote <- rmsg
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								bridge/xmpp/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								bridge/xmpp/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					package bxmpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var pathRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetAvatar constructs a URL for a given user-avatar if it is available in the cache.
 | 
				
			||||||
 | 
					func getAvatar(av map[string]string, userid string, general *config.Protocol) string {
 | 
				
			||||||
 | 
						if hash, ok := av[userid]; ok {
 | 
				
			||||||
 | 
							// NOTE: This does not happen in bridge/helper/helper.go but messes up XMPP
 | 
				
			||||||
 | 
							id := pathRegex.ReplaceAllString(userid, "_")
 | 
				
			||||||
 | 
							return general.MediaServerDownload + "/" + hash + "/" + id + ".png"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) cacheAvatar(msg *config.Message) string {
 | 
				
			||||||
 | 
						fi := msg.Extra["file"][0].(config.FileInfo)
 | 
				
			||||||
 | 
						/* if we have a sha we have successfully uploaded the file to the media server,
 | 
				
			||||||
 | 
						so we can now cache the sha */
 | 
				
			||||||
 | 
						if fi.SHA != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
 | 
				
			||||||
 | 
							b.avatarMap[msg.UserID] = fi.SHA
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										461
									
								
								bridge/xmpp/xmpp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										461
									
								
								bridge/xmpp/xmpp.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,461 @@
 | 
				
			|||||||
 | 
					package bxmpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/jpillora/backoff"
 | 
				
			||||||
 | 
						"github.com/matterbridge/go-xmpp"
 | 
				
			||||||
 | 
						"github.com/rs/xid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bxmpp struct {
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						startTime time.Time
 | 
				
			||||||
 | 
						xc        *xmpp.Client
 | 
				
			||||||
 | 
						xmppMap   map[string]string
 | 
				
			||||||
 | 
						connected bool
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						avatarAvailability map[string]bool
 | 
				
			||||||
 | 
						avatarMap          map[string]string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						return &Bxmpp{
 | 
				
			||||||
 | 
							Config:             cfg,
 | 
				
			||||||
 | 
							xmppMap:            make(map[string]string),
 | 
				
			||||||
 | 
							avatarAvailability: make(map[string]bool),
 | 
				
			||||||
 | 
							avatarMap:          make(map[string]string),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) Connect() error {
 | 
				
			||||||
 | 
						b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
				
			||||||
 | 
						if err := b.createXMPP(); err != nil {
 | 
				
			||||||
 | 
							b.Log.Debugf("%#v", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						go b.manageConnection()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						if channel.Options.Key != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
 | 
				
			||||||
 | 
							b.xc.JoinProtectedMUC(channel.Name+"@"+b.GetString("Muc"), b.GetString("Nick"), channel.Options.Key, xmpp.NoHistory, 0, nil)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							b.xc.JoinMUCNoHistory(channel.Name+"@"+b.GetString("Muc"), b.GetString("Nick"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						// should be fixed by using a cache instead of dropping
 | 
				
			||||||
 | 
						if !b.Connected() {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("bridge %s not connected, dropping message %#v to bridge", b.Account, msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// ignore delete messages
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Event == config.EventAvatarDownload {
 | 
				
			||||||
 | 
							return b.cacheAvatar(&msg), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make a action /me of the message, prepend the username with it.
 | 
				
			||||||
 | 
						// https://xmpp.org/extensions/xep-0245.html
 | 
				
			||||||
 | 
						if msg.Event == config.EventUserAction {
 | 
				
			||||||
 | 
							msg.Username = "/me " + msg.Username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								b.Log.Debugf("=> Sending attachement message %#v", rmsg)
 | 
				
			||||||
 | 
								if b.GetString("WebhookURL") != "" {
 | 
				
			||||||
 | 
									err = b.postSlackCompatibleWebhook(msg)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									_, err = b.xc.Send(xmpp.Chat{
 | 
				
			||||||
 | 
										Type:   "groupchat",
 | 
				
			||||||
 | 
										Remote: rmsg.Channel + "@" + b.GetString("Muc"),
 | 
				
			||||||
 | 
										Text:   rmsg.Username + rmsg.Text,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									b.Log.WithError(err).Error("Unable to send message with share URL.")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return "", b.handleUploadFile(&msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if b.GetString("WebhookURL") != "" {
 | 
				
			||||||
 | 
							b.Log.Debugf("Sending message using Webhook")
 | 
				
			||||||
 | 
							err := b.postSlackCompatibleWebhook(msg)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								b.Log.Errorf("Failed to send message using webhook: %s", err)
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post normal message.
 | 
				
			||||||
 | 
						var msgReplaceID string
 | 
				
			||||||
 | 
						msgID := xid.New().String()
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							msgReplaceID = msg.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Sending message %#v", msg)
 | 
				
			||||||
 | 
						if _, err := b.xc.Send(xmpp.Chat{
 | 
				
			||||||
 | 
							Type:      "groupchat",
 | 
				
			||||||
 | 
							Remote:    msg.Channel + "@" + b.GetString("Muc"),
 | 
				
			||||||
 | 
							Text:      msg.Username + msg.Text,
 | 
				
			||||||
 | 
							ID:        msgID,
 | 
				
			||||||
 | 
							ReplaceID: msgReplaceID,
 | 
				
			||||||
 | 
						}); err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return msgID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error {
 | 
				
			||||||
 | 
						type XMPPWebhook struct {
 | 
				
			||||||
 | 
							Username string `json:"username"`
 | 
				
			||||||
 | 
							Text     string `json:"text"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						webhookBody, err := json.Marshal(XMPPWebhook{
 | 
				
			||||||
 | 
							Username: msg.Username,
 | 
				
			||||||
 | 
							Text:     msg.Text,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Failed to marshal webhook: %s", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := http.Post(b.GetString("WebhookURL")+"/"+url.QueryEscape(msg.Channel), "application/json", bytes.NewReader(webhookBody))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Failed to POST webhook: %s", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp.Body.Close()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) createXMPP() error {
 | 
				
			||||||
 | 
						var serverName string
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case !b.GetBool("Anonymous"):
 | 
				
			||||||
 | 
							if !strings.Contains(b.GetString("Jid"), "@") {
 | 
				
			||||||
 | 
								return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							serverName = strings.Split(b.GetString("Jid"), "@")[1]
 | 
				
			||||||
 | 
						case !strings.Contains(b.GetString("Server"), ":"):
 | 
				
			||||||
 | 
							serverName = strings.Split(b.GetString("Server"), ":")[0]
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							serverName = b.GetString("Server")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tc := &tls.Config{
 | 
				
			||||||
 | 
							ServerName:         serverName,
 | 
				
			||||||
 | 
							InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						xmpp.DebugWriter = b.Log.Writer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						options := xmpp.Options{
 | 
				
			||||||
 | 
							Host:                         b.GetString("Server"),
 | 
				
			||||||
 | 
							User:                         b.GetString("Jid"),
 | 
				
			||||||
 | 
							Password:                     b.GetString("Password"),
 | 
				
			||||||
 | 
							NoTLS:                        true,
 | 
				
			||||||
 | 
							StartTLS:                     !b.GetBool("NoTLS"),
 | 
				
			||||||
 | 
							TLSConfig:                    tc,
 | 
				
			||||||
 | 
							Debug:                        b.GetBool("debug"),
 | 
				
			||||||
 | 
							Session:                      true,
 | 
				
			||||||
 | 
							Status:                       "",
 | 
				
			||||||
 | 
							StatusMessage:                "",
 | 
				
			||||||
 | 
							Resource:                     "",
 | 
				
			||||||
 | 
							InsecureAllowUnencryptedAuth: b.GetBool("NoTLS"),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						b.xc, err = options.NewClient()
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) manageConnection() {
 | 
				
			||||||
 | 
						b.setConnected(true)
 | 
				
			||||||
 | 
						initial := true
 | 
				
			||||||
 | 
						bf := &backoff.Backoff{
 | 
				
			||||||
 | 
							Min:    time.Second,
 | 
				
			||||||
 | 
							Max:    5 * time.Minute,
 | 
				
			||||||
 | 
							Jitter: true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Main connection loop. Each iteration corresponds to a successful
 | 
				
			||||||
 | 
						// connection attempt and the subsequent handling of the connection.
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							if initial {
 | 
				
			||||||
 | 
								initial = false
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								b.Remote <- config.Message{
 | 
				
			||||||
 | 
									Username: "system",
 | 
				
			||||||
 | 
									Text:     "rejoin",
 | 
				
			||||||
 | 
									Channel:  "",
 | 
				
			||||||
 | 
									Account:  b.Account,
 | 
				
			||||||
 | 
									Event:    config.EventRejoinChannels,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := b.handleXMPP(); err != nil {
 | 
				
			||||||
 | 
								b.Log.WithError(err).Error("Disconnected.")
 | 
				
			||||||
 | 
								b.setConnected(false)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Reconnection loop using an exponential back-off strategy. We
 | 
				
			||||||
 | 
							// only break out of the loop if we have successfully reconnected.
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								d := bf.Duration()
 | 
				
			||||||
 | 
								b.Log.Infof("Reconnecting in %s.", d)
 | 
				
			||||||
 | 
								time.Sleep(d)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								b.Log.Infof("Reconnecting now.")
 | 
				
			||||||
 | 
								if err := b.createXMPP(); err == nil {
 | 
				
			||||||
 | 
									b.setConnected(true)
 | 
				
			||||||
 | 
									bf.Reset()
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.Log.Warn("Failed to reconnect.")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) xmppKeepAlive() chan bool {
 | 
				
			||||||
 | 
						done := make(chan bool)
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							ticker := time.NewTicker(90 * time.Second)
 | 
				
			||||||
 | 
							defer ticker.Stop()
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								select {
 | 
				
			||||||
 | 
								case <-ticker.C:
 | 
				
			||||||
 | 
									b.Log.Debugf("PING")
 | 
				
			||||||
 | 
									if err := b.xc.PingC2S("", ""); err != nil {
 | 
				
			||||||
 | 
										b.Log.Debugf("PING failed %#v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								case <-done:
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						return done
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) handleXMPP() error {
 | 
				
			||||||
 | 
						b.startTime = time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						done := b.xmppKeepAlive()
 | 
				
			||||||
 | 
						defer close(done)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							m, err := b.xc.Recv()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								// An error together with AvatarData is non-fatal
 | 
				
			||||||
 | 
								switch m.(type) {
 | 
				
			||||||
 | 
								case xmpp.AvatarData:
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch v := m.(type) {
 | 
				
			||||||
 | 
							case xmpp.Chat:
 | 
				
			||||||
 | 
								if v.Type == "groupchat" {
 | 
				
			||||||
 | 
									b.Log.Debugf("== Receiving %#v", v)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Skip invalid messages.
 | 
				
			||||||
 | 
									if b.skipMessage(v) {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									var event string
 | 
				
			||||||
 | 
									if strings.Contains(v.Text, "has set the subject to:") {
 | 
				
			||||||
 | 
										event = config.EventTopicChange
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									available, sok := b.avatarAvailability[v.Remote]
 | 
				
			||||||
 | 
									avatar := ""
 | 
				
			||||||
 | 
									if !sok {
 | 
				
			||||||
 | 
										b.Log.Debugf("Requesting avatar data")
 | 
				
			||||||
 | 
										b.avatarAvailability[v.Remote] = false
 | 
				
			||||||
 | 
										b.xc.AvatarRequestData(v.Remote)
 | 
				
			||||||
 | 
									} else if available {
 | 
				
			||||||
 | 
										avatar = getAvatar(b.avatarMap, v.Remote, b.General)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									msgID := v.ID
 | 
				
			||||||
 | 
									if v.ReplaceID != "" {
 | 
				
			||||||
 | 
										msgID = v.ReplaceID
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									rmsg := config.Message{
 | 
				
			||||||
 | 
										Username: b.parseNick(v.Remote),
 | 
				
			||||||
 | 
										Text:     v.Text,
 | 
				
			||||||
 | 
										Channel:  b.parseChannel(v.Remote),
 | 
				
			||||||
 | 
										Account:  b.Account,
 | 
				
			||||||
 | 
										Avatar:   avatar,
 | 
				
			||||||
 | 
										UserID:   v.Remote,
 | 
				
			||||||
 | 
										ID:       msgID,
 | 
				
			||||||
 | 
										Event:    event,
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Check if we have an action event.
 | 
				
			||||||
 | 
									var ok bool
 | 
				
			||||||
 | 
									rmsg.Text, ok = b.replaceAction(rmsg.Text)
 | 
				
			||||||
 | 
									if ok {
 | 
				
			||||||
 | 
										rmsg.Event = config.EventUserAction
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
 | 
				
			||||||
 | 
									b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
									b.Remote <- rmsg
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case xmpp.AvatarData:
 | 
				
			||||||
 | 
								b.handleDownloadAvatar(v)
 | 
				
			||||||
 | 
								b.avatarAvailability[v.From] = true
 | 
				
			||||||
 | 
								b.Log.Debugf("Avatar for %s is now available", v.From)
 | 
				
			||||||
 | 
							case xmpp.Presence:
 | 
				
			||||||
 | 
								// Do nothing.
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) replaceAction(text string) (string, bool) {
 | 
				
			||||||
 | 
						if strings.HasPrefix(text, "/me ") {
 | 
				
			||||||
 | 
							return strings.Replace(text, "/me ", "", -1), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return text, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleUploadFile handles native upload of files
 | 
				
			||||||
 | 
					func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
 | 
				
			||||||
 | 
						var urlDesc string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, file := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fileInfo := file.(config.FileInfo)
 | 
				
			||||||
 | 
							if fileInfo.Comment != "" {
 | 
				
			||||||
 | 
								msg.Text += fileInfo.Comment + ": "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fileInfo.URL != "" {
 | 
				
			||||||
 | 
								msg.Text = fileInfo.URL
 | 
				
			||||||
 | 
								if fileInfo.Comment != "" {
 | 
				
			||||||
 | 
									msg.Text = fileInfo.Comment + ": " + fileInfo.URL
 | 
				
			||||||
 | 
									urlDesc = fileInfo.Comment
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if _, err := b.xc.Send(xmpp.Chat{
 | 
				
			||||||
 | 
								Type:   "groupchat",
 | 
				
			||||||
 | 
								Remote: msg.Channel + "@" + b.GetString("Muc"),
 | 
				
			||||||
 | 
								Text:   msg.Username + msg.Text,
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if fileInfo.URL != "" {
 | 
				
			||||||
 | 
								if _, err := b.xc.SendOOB(xmpp.Chat{
 | 
				
			||||||
 | 
									Type:    "groupchat",
 | 
				
			||||||
 | 
									Remote:  msg.Channel + "@" + b.GetString("Muc"),
 | 
				
			||||||
 | 
									Ooburl:  fileInfo.URL,
 | 
				
			||||||
 | 
									Oobdesc: urlDesc,
 | 
				
			||||||
 | 
								}); err != nil {
 | 
				
			||||||
 | 
									b.Log.WithError(err).Warn("Failed to send share URL.")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) parseNick(remote string) string {
 | 
				
			||||||
 | 
						s := strings.Split(remote, "@")
 | 
				
			||||||
 | 
						if len(s) > 1 {
 | 
				
			||||||
 | 
							s = strings.Split(s[1], "/")
 | 
				
			||||||
 | 
							if len(s) == 2 {
 | 
				
			||||||
 | 
								return s[1] // nick
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) parseChannel(remote string) string {
 | 
				
			||||||
 | 
						s := strings.Split(remote, "@")
 | 
				
			||||||
 | 
						if len(s) >= 2 {
 | 
				
			||||||
 | 
							return s[0] // channel
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// skipMessage skips messages that need to be skipped
 | 
				
			||||||
 | 
					func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
 | 
				
			||||||
 | 
						// skip messages from ourselves
 | 
				
			||||||
 | 
						if b.parseNick(message.Remote) == b.GetString("Nick") {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// skip empty messages
 | 
				
			||||||
 | 
						if message.Text == "" {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// skip subject messages
 | 
				
			||||||
 | 
						if strings.Contains(message.Text, "</subject>") {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// do not show subjects on connect #732
 | 
				
			||||||
 | 
						if strings.Contains(message.Text, "has set the subject to:") && time.Since(b.startTime) < time.Second*5 {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore messages posted by our webhook
 | 
				
			||||||
 | 
						if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// skip delayed messages
 | 
				
			||||||
 | 
						return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) setConnected(state bool) {
 | 
				
			||||||
 | 
						b.Lock()
 | 
				
			||||||
 | 
						b.connected = state
 | 
				
			||||||
 | 
						defer b.Unlock()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bxmpp) Connected() bool {
 | 
				
			||||||
 | 
						b.RLock()
 | 
				
			||||||
 | 
						defer b.RUnlock()
 | 
				
			||||||
 | 
						return b.connected
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										216
									
								
								bridge/zulip/zulip.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								bridge/zulip/zulip.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,216 @@
 | 
				
			|||||||
 | 
					package bzulip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/config"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/helper"
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/version"
 | 
				
			||||||
 | 
						gzb "github.com/matterbridge/gozulipbot"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Bzulip struct {
 | 
				
			||||||
 | 
						q       *gzb.Queue
 | 
				
			||||||
 | 
						bot     *gzb.Bot
 | 
				
			||||||
 | 
						streams map[int]string
 | 
				
			||||||
 | 
						*bridge.Config
 | 
				
			||||||
 | 
						sync.RWMutex
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(cfg *bridge.Config) bridge.Bridger {
 | 
				
			||||||
 | 
						return &Bzulip{Config: cfg, streams: make(map[int]string)}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) Connect() error {
 | 
				
			||||||
 | 
						bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login"), UserAgent: fmt.Sprintf("matterbridge/%s", version.Release)}
 | 
				
			||||||
 | 
						bot.Init()
 | 
				
			||||||
 | 
						q, err := bot.RegisterAll()
 | 
				
			||||||
 | 
						b.q = q
 | 
				
			||||||
 | 
						b.bot = &bot
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("Connect() %#v", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// init stream
 | 
				
			||||||
 | 
						b.getChannel(0)
 | 
				
			||||||
 | 
						b.Log.Info("Connection succeeded")
 | 
				
			||||||
 | 
						go b.handleQueue()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) Disconnect() error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) JoinChannel(channel config.ChannelInfo) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) Send(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						b.Log.Debugf("=> Receiving %#v", msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delete message
 | 
				
			||||||
 | 
						if msg.Event == config.EventMsgDelete {
 | 
				
			||||||
 | 
							if msg.ID == "" {
 | 
				
			||||||
 | 
								return "", nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_, err := b.bot.UpdateMessage(msg.ID, "")
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Upload a file if it exists
 | 
				
			||||||
 | 
						if msg.Extra != nil {
 | 
				
			||||||
 | 
							for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
				
			||||||
 | 
								b.sendMessage(rmsg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(msg.Extra["file"]) > 0 {
 | 
				
			||||||
 | 
								return b.handleUploadFile(&msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// edit the message if we have a msg ID
 | 
				
			||||||
 | 
						if msg.ID != "" {
 | 
				
			||||||
 | 
							_, err := b.bot.UpdateMessage(msg.ID, msg.Username+msg.Text)
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Post normal message
 | 
				
			||||||
 | 
						return b.sendMessage(msg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) getChannel(id int) string {
 | 
				
			||||||
 | 
						if name, ok := b.streams[id]; ok {
 | 
				
			||||||
 | 
							return name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						streams, err := b.bot.GetRawStreams()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							b.Log.Errorf("getChannel: %#v", err)
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, stream := range streams.Streams {
 | 
				
			||||||
 | 
							b.streams[stream.StreamID] = stream.Name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if name, ok := b.streams[id]; ok {
 | 
				
			||||||
 | 
							return name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) handleQueue() error {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							messages, err := b.q.GetEvents()
 | 
				
			||||||
 | 
							switch err {
 | 
				
			||||||
 | 
							case gzb.BackoffError:
 | 
				
			||||||
 | 
								time.Sleep(time.Second * 5)
 | 
				
			||||||
 | 
							case gzb.NoJSONError:
 | 
				
			||||||
 | 
								b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
 | 
				
			||||||
 | 
								time.Sleep(time.Second * 10)
 | 
				
			||||||
 | 
							case gzb.BadEventQueueError:
 | 
				
			||||||
 | 
								b.Log.Info("got a bad event queue id error, reconnecting")
 | 
				
			||||||
 | 
								b.bot.Queues = nil
 | 
				
			||||||
 | 
								for {
 | 
				
			||||||
 | 
									b.q, err = b.bot.RegisterAll()
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
 | 
				
			||||||
 | 
										time.Sleep(time.Second * 10)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case gzb.HeartbeatError:
 | 
				
			||||||
 | 
								b.Log.Debug("heartbeat received.")
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								b.Log.Debugf("receiving error: %#v", err)
 | 
				
			||||||
 | 
								time.Sleep(time.Second * 10)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, m := range messages {
 | 
				
			||||||
 | 
								b.Log.Debugf("== Receiving %#v", m)
 | 
				
			||||||
 | 
								// ignore our own messages
 | 
				
			||||||
 | 
								if m.SenderEmail == b.GetString("login") {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								avatarURL := m.AvatarURL
 | 
				
			||||||
 | 
								if !strings.HasPrefix(avatarURL, "http") {
 | 
				
			||||||
 | 
									avatarURL = b.GetString("server") + avatarURL
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								rmsg := config.Message{
 | 
				
			||||||
 | 
									Username: m.SenderFullName,
 | 
				
			||||||
 | 
									Text:     m.Content,
 | 
				
			||||||
 | 
									Channel:  b.getChannel(m.StreamID) + "/topic:" + m.Subject,
 | 
				
			||||||
 | 
									Account:  b.Account,
 | 
				
			||||||
 | 
									UserID:   strconv.Itoa(m.SenderID),
 | 
				
			||||||
 | 
									Avatar:   avatarURL,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
 | 
				
			||||||
 | 
								b.Log.Debugf("<= Message is %#v", rmsg)
 | 
				
			||||||
 | 
								b.Remote <- rmsg
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							time.Sleep(time.Second * 3)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) sendMessage(msg config.Message) (string, error) {
 | 
				
			||||||
 | 
						topic := ""
 | 
				
			||||||
 | 
						if strings.Contains(msg.Channel, "/topic:") {
 | 
				
			||||||
 | 
							res := strings.Split(msg.Channel, "/topic:")
 | 
				
			||||||
 | 
							topic = res[1]
 | 
				
			||||||
 | 
							msg.Channel = res[0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m := gzb.Message{
 | 
				
			||||||
 | 
							Stream:  msg.Channel,
 | 
				
			||||||
 | 
							Topic:   topic,
 | 
				
			||||||
 | 
							Content: msg.Username + msg.Text,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resp, err := b.bot.Message(m)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if resp != nil {
 | 
				
			||||||
 | 
							defer resp.Body.Close()
 | 
				
			||||||
 | 
							res, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							var jr struct {
 | 
				
			||||||
 | 
								ID int `json:"id"`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = json.Unmarshal(res, &jr)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return strconv.Itoa(jr.ID), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Bzulip) handleUploadFile(msg *config.Message) (string, error) {
 | 
				
			||||||
 | 
						for _, f := range msg.Extra["file"] {
 | 
				
			||||||
 | 
							fi := f.(config.FileInfo)
 | 
				
			||||||
 | 
							if fi.Comment != "" {
 | 
				
			||||||
 | 
								msg.Text += fi.Comment + ": "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fi.URL != "" {
 | 
				
			||||||
 | 
								msg.Text = fi.URL
 | 
				
			||||||
 | 
								if fi.Comment != "" {
 | 
				
			||||||
 | 
									msg.Text = fi.Comment + ": " + fi.URL
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_, err := b.sendMessage(*msg)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1775
									
								
								changelog.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1775
									
								
								changelog.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										210
									
								
								contrib/api.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								contrib/api.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
				
			|||||||
 | 
					openapi: 3.0.0
 | 
				
			||||||
 | 
					info:
 | 
				
			||||||
 | 
					  contact: {}
 | 
				
			||||||
 | 
					  description: A read/write API for the Matterbridge chat bridge.
 | 
				
			||||||
 | 
					  license:
 | 
				
			||||||
 | 
					    name: Apache 2.0
 | 
				
			||||||
 | 
					    url: 'https://github.com/42wim/matterbridge/blob/master/LICENSE'
 | 
				
			||||||
 | 
					  title: Matterbridge API
 | 
				
			||||||
 | 
					  version: "0.1.0-oas3"
 | 
				
			||||||
 | 
					paths:
 | 
				
			||||||
 | 
					  /health:
 | 
				
			||||||
 | 
					    get:
 | 
				
			||||||
 | 
					      responses:
 | 
				
			||||||
 | 
					        '200':
 | 
				
			||||||
 | 
					          description: OK
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            '*/*':
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                type: string
 | 
				
			||||||
 | 
					      summary: Checks if the server is alive.
 | 
				
			||||||
 | 
					  /message:
 | 
				
			||||||
 | 
					    post:
 | 
				
			||||||
 | 
					      responses:
 | 
				
			||||||
 | 
					        '200':
 | 
				
			||||||
 | 
					          description: OK
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                $ref: '#/components/schemas/config.OutgoingMessageResponse'
 | 
				
			||||||
 | 
					      summary: Create a message
 | 
				
			||||||
 | 
					      requestBody:
 | 
				
			||||||
 | 
					        content:
 | 
				
			||||||
 | 
					          application/json:
 | 
				
			||||||
 | 
					            schema:
 | 
				
			||||||
 | 
					              $ref: '#/components/schemas/config.OutgoingMessage'
 | 
				
			||||||
 | 
					        description: Message object to create
 | 
				
			||||||
 | 
					        required: true
 | 
				
			||||||
 | 
					  /messages:
 | 
				
			||||||
 | 
					    get:
 | 
				
			||||||
 | 
					      responses:
 | 
				
			||||||
 | 
					        '200':
 | 
				
			||||||
 | 
					          description: OK
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                items:
 | 
				
			||||||
 | 
					                  $ref: '#/components/schemas/config.IncomingMessage'
 | 
				
			||||||
 | 
					                type: array
 | 
				
			||||||
 | 
					      security:
 | 
				
			||||||
 | 
					        - ApiKeyAuth: []
 | 
				
			||||||
 | 
					      summary: List new messages
 | 
				
			||||||
 | 
					  /stream:
 | 
				
			||||||
 | 
					    get:
 | 
				
			||||||
 | 
					      responses:
 | 
				
			||||||
 | 
					        '200':
 | 
				
			||||||
 | 
					          description: OK
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/x-json-stream:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                $ref: '#/components/schemas/config.IncomingMessage'
 | 
				
			||||||
 | 
					      summary: Stream realtime messages
 | 
				
			||||||
 | 
					servers:
 | 
				
			||||||
 | 
					  - url: /api
 | 
				
			||||||
 | 
					components:
 | 
				
			||||||
 | 
					  securitySchemes:
 | 
				
			||||||
 | 
					    bearerAuth:
 | 
				
			||||||
 | 
					      type: http
 | 
				
			||||||
 | 
					      scheme: bearer
 | 
				
			||||||
 | 
					  schemas:
 | 
				
			||||||
 | 
					    config.IncomingMessage:
 | 
				
			||||||
 | 
					      properties:
 | 
				
			||||||
 | 
					        avatar:
 | 
				
			||||||
 | 
					          description: URL to an avatar image
 | 
				
			||||||
 | 
					          example: >-
 | 
				
			||||||
 | 
					            https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        event:
 | 
				
			||||||
 | 
					          description: >-
 | 
				
			||||||
 | 
					            A specific matterbridge event. (see
 | 
				
			||||||
 | 
					            https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        gateway:
 | 
				
			||||||
 | 
					          description: Name of the gateway as configured in matterbridge.toml
 | 
				
			||||||
 | 
					          example: mygateway
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        text:
 | 
				
			||||||
 | 
					          description: Content of the message
 | 
				
			||||||
 | 
					          example: 'Testing, testing, 1-2-3.'
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        username:
 | 
				
			||||||
 | 
					          description: Human-readable username
 | 
				
			||||||
 | 
					          example: alice
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        account:
 | 
				
			||||||
 | 
					          description: Unique account name of format "[protocol].[slug]" as defined in matterbridge.toml 
 | 
				
			||||||
 | 
					          example: slack.myteam
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        channel:
 | 
				
			||||||
 | 
					          description: Human-readable channel name of sending bridge
 | 
				
			||||||
 | 
					          example: test-channel
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        id:
 | 
				
			||||||
 | 
					          description: Unique ID of message on the gateway
 | 
				
			||||||
 | 
					          example: slack 1541361213.030700
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        parent_id:
 | 
				
			||||||
 | 
					          description: Unique ID of a parent message, if threaded
 | 
				
			||||||
 | 
					          example: slack 1541361213.030700
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        protocol:
 | 
				
			||||||
 | 
					          description: Chat protocol of the sending bridge
 | 
				
			||||||
 | 
					          example: slack
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        timestamp:
 | 
				
			||||||
 | 
					          description: Timestamp of the message
 | 
				
			||||||
 | 
					          example: "1541361213.030700"
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        userid:
 | 
				
			||||||
 | 
					          description: Userid on the sending bridge
 | 
				
			||||||
 | 
					          example: U4MCXJKNC
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        extra:
 | 
				
			||||||
 | 
					          description: Extra data that doesn't fit in other fields (eg base64 encoded files)
 | 
				
			||||||
 | 
					          type: object
 | 
				
			||||||
 | 
					    config.OutgoingMessage:
 | 
				
			||||||
 | 
					      properties:
 | 
				
			||||||
 | 
					        avatar:
 | 
				
			||||||
 | 
					          description: URL to an avatar image
 | 
				
			||||||
 | 
					          example: >-
 | 
				
			||||||
 | 
					            https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        event:
 | 
				
			||||||
 | 
					          description: >-
 | 
				
			||||||
 | 
					            A specific matterbridge event. (see
 | 
				
			||||||
 | 
					            https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
 | 
				
			||||||
 | 
					          example: ""
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        gateway:
 | 
				
			||||||
 | 
					          description: Name of the gateway as configured in matterbridge.toml
 | 
				
			||||||
 | 
					          example: mygateway
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        text:
 | 
				
			||||||
 | 
					          description: Content of the message
 | 
				
			||||||
 | 
					          example: 'Testing, testing, 1-2-3.'
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        username:
 | 
				
			||||||
 | 
					          description: Human-readable username
 | 
				
			||||||
 | 
					          example: alice
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					      type: object
 | 
				
			||||||
 | 
					      required:
 | 
				
			||||||
 | 
					        - gateway
 | 
				
			||||||
 | 
					        - text
 | 
				
			||||||
 | 
					        - username
 | 
				
			||||||
 | 
					    config.OutgoingMessageResponse:
 | 
				
			||||||
 | 
					      properties:
 | 
				
			||||||
 | 
					        avatar:
 | 
				
			||||||
 | 
					          description: URL to an avatar image
 | 
				
			||||||
 | 
					          example: >-
 | 
				
			||||||
 | 
					            https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        event:
 | 
				
			||||||
 | 
					          description: >-
 | 
				
			||||||
 | 
					            A specific matterbridge event. (see
 | 
				
			||||||
 | 
					            https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
 | 
				
			||||||
 | 
					          example: ""
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        gateway:
 | 
				
			||||||
 | 
					          description: Name of the gateway as configured in matterbridge.toml
 | 
				
			||||||
 | 
					          example: mygateway
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        text:
 | 
				
			||||||
 | 
					          description: Content of the message
 | 
				
			||||||
 | 
					          example: 'Testing, testing, 1-2-3.'
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        username:
 | 
				
			||||||
 | 
					          description: Human-readable username
 | 
				
			||||||
 | 
					          example: alice
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        account:
 | 
				
			||||||
 | 
					          description: fixed api account 
 | 
				
			||||||
 | 
					          example: api.local
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        channel:
 | 
				
			||||||
 | 
					          description: fixed api channel 
 | 
				
			||||||
 | 
					          example: api
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        id:
 | 
				
			||||||
 | 
					          example: ""
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        parent_id:
 | 
				
			||||||
 | 
					          example: ""
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        protocol:
 | 
				
			||||||
 | 
					          description: fixed api protocol
 | 
				
			||||||
 | 
					          example: api
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        timestamp:
 | 
				
			||||||
 | 
					          description: Timestamp of the message
 | 
				
			||||||
 | 
					          example: "1541361213.030700"
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        userid:
 | 
				
			||||||
 | 
					          example: ""
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        extra:
 | 
				
			||||||
 | 
					          example: null
 | 
				
			||||||
 | 
					          type: object
 | 
				
			||||||
 | 
					      type: object
 | 
				
			||||||
 | 
					security:
 | 
				
			||||||
 | 
					  - bearerAuth: []
 | 
				
			||||||
							
								
								
									
										2
									
								
								contrib/example.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								contrib/example.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					text := import("text")
 | 
				
			||||||
 | 
					msgText=text.re_replace("matterbridge",msgText,"matterbridge (https://github.com/42wim/matterbridge)")
 | 
				
			||||||
							
								
								
									
										15
									
								
								contrib/inmessage-logger.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								contrib/inmessage-logger.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					fmt := import("fmt")
 | 
				
			||||||
 | 
					os := import("os")
 | 
				
			||||||
 | 
					times := import("times")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if msgText != "" && msgUsername != "system" {
 | 
				
			||||||
 | 
					    os.chdir("/var/www/matterbridge")
 | 
				
			||||||
 | 
					    file := os.open_file("inmessage.log", os.o_append|os.o_wronly|os.o_create, 0644)
 | 
				
			||||||
 | 
					    file.write_string(fmt.sprintf(
 | 
				
			||||||
 | 
					        "[%s] <%s> %s\n",
 | 
				
			||||||
 | 
					        times.time_format(times.now(), times.format_rfc1123),
 | 
				
			||||||
 | 
					        msgUsername,
 | 
				
			||||||
 | 
					        msgText
 | 
				
			||||||
 | 
					    ))
 | 
				
			||||||
 | 
					    file.close()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								contrib/matterbridge.openrc
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										19
									
								
								contrib/matterbridge.openrc
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					#!/sbin/openrc-run
 | 
				
			||||||
 | 
					# Copyright 2021-2022 Gentoo Authors
 | 
				
			||||||
 | 
					# Distributed under the terms of the GNU General Public License v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					command=/usr/bin/matterbridge
 | 
				
			||||||
 | 
					command_args="-conf ${MATTERBRIDGE_CONF:-/etc/matterbridge/bridge.toml} ${MATTERBRIDGE_ARGS}"
 | 
				
			||||||
 | 
					command_user="matterbridge:matterbridge"
 | 
				
			||||||
 | 
					pidfile="/run/${RC_SVCNAME}.pid"
 | 
				
			||||||
 | 
					command_background=1
 | 
				
			||||||
 | 
					output_log="/var/log/${RC_SVCNAME}.log"
 | 
				
			||||||
 | 
					error_log="${output_log}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					depend() {
 | 
				
			||||||
 | 
						need net
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					start_pre() {
 | 
				
			||||||
 | 
						checkpath -f "${output_log}" -o "${command_user}" || return 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								contrib/matterbridge.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								contrib/matterbridge.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[Unit]
 | 
				
			||||||
 | 
					Description=matterbridge
 | 
				
			||||||
 | 
					After=network.target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Service]
 | 
				
			||||||
 | 
					ExecStart=/usr/bin/matterbridge -conf /etc/matterbridge/bridge.toml
 | 
				
			||||||
 | 
					User=matterbridge
 | 
				
			||||||
 | 
					Group=matterbridge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Install]
 | 
				
			||||||
 | 
					WantedBy=multi-user.target
 | 
				
			||||||
							
								
								
									
										10
									
								
								contrib/outmessage-discordemoji.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								contrib/outmessage-discordemoji.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					text := import("text")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// if we're not sending to a discord bridge,
 | 
				
			||||||
 | 
					// then convert custom emoji tags into url's
 | 
				
			||||||
 | 
					if (inProtocol == "discord" && outProtocol != "discord") {
 | 
				
			||||||
 | 
					    rePNG := text.re_compile(`<:.*?:([0-9]+)>`)
 | 
				
			||||||
 | 
					    msgText=rePNG.replace(msgText,"https://cdn.discordapp.com/emojis/$1.png")
 | 
				
			||||||
 | 
					    reGIF := text.re_compile(`<a:.*?:([0-9]+)>`)
 | 
				
			||||||
 | 
					    msgText=reGIF.replace(msgText,"https://cdn.discordapp.com/emojis/$1.gif")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								contrib/outmessage-irccolornick.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								contrib/outmessage-irccolornick.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					// See https://github.com/42wim/matterbridge/issues/881
 | 
				
			||||||
 | 
					// Generates a colored nick for each msgUsername, with example to filter specific codes 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					text := import("text")
 | 
				
			||||||
 | 
					fmt := import("fmt")
 | 
				
			||||||
 | 
					if outProtocol == "irc" {
 | 
				
			||||||
 | 
					    // generate a color for a nick, make sure it isn't 0 or 15
 | 
				
			||||||
 | 
					    colorCode := len(msgUsername)+bytes(msgUsername)[0]%14 + 2
 | 
				
			||||||
 | 
					    // example if we want to use colorCode 3 when we have calculated colorcode 14
 | 
				
			||||||
 | 
					    if colorCode == 14 {
 | 
				
			||||||
 | 
					        colorCode = 3
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    msgUsername=fmt.sprintf("\x03%02d%s\x0F", colorCode, msgUsername)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								contrib/outmessage-irccolors.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								contrib/outmessage-irccolors.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					// See https://github.com/42wim/matterbridge/issues/798
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// if we're not sending to an irc bridge we strip the IRC colors
 | 
				
			||||||
 | 
					if outProtocol != "irc" {
 | 
				
			||||||
 | 
					    re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
 | 
				
			||||||
 | 
					    msgText=re.replace(msgText,"")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								contrib/remotenickformat-zerowidth.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								contrib/remotenickformat-zerowidth.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					This script will return the nick except with multi-character usernames
 | 
				
			||||||
 | 
					containing a zero-width space between the first and second character letter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Single character usernames will be left untouched.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is useful to prevent remote users from nickalerting
 | 
				
			||||||
 | 
					IRC users of the same name when the remote user speaks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This result can be used in {TENGO} in RemoteNickFormat.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					result = nick
 | 
				
			||||||
 | 
					if len(nick) > 1 {
 | 
				
			||||||
 | 
					    result = string(nick[0]) + "" + nick[1:]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								contrib/remotenickformat.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								contrib/remotenickformat.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					This script will return the current time in kitchen format if the protocol (of the remote bridge) isn't irc
 | 
				
			||||||
 | 
					See https://github.com/d5/tengo/blob/master/docs/stdlib-times.md
 | 
				
			||||||
 | 
					This result can be used in {TENGO} in RemoteNickFormat
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					times := import("times")
 | 
				
			||||||
 | 
					if protocol != "irc" {
 | 
				
			||||||
 | 
					   result=times.time_format(times.now(),times.format_kitchen)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								docker/arm/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								docker/arm/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					FROM alpine:edge as certs
 | 
				
			||||||
 | 
					RUN apk --update add ca-certificates
 | 
				
			||||||
 | 
					ARG VERSION=1.22.3
 | 
				
			||||||
 | 
					ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-${VERSION}-linux-arm64 /bin/matterbridge
 | 
				
			||||||
 | 
					RUN chmod +x /bin/matterbridge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FROM scratch
 | 
				
			||||||
 | 
					COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
 | 
				
			||||||
 | 
					COPY --from=certs /bin/matterbridge /bin/matterbridge
 | 
				
			||||||
 | 
					ENTRYPOINT ["/bin/matterbridge"]
 | 
				
			||||||
							
								
								
									
										5
									
								
								gateway/bench.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								gateway/bench.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					text := import("text")
 | 
				
			||||||
 | 
					if text.re_match("blah",msgText) {
 | 
				
			||||||
 | 
					    msgText="replaced by this"
 | 
				
			||||||
 | 
					    msgUsername="fakeuser"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/api.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !noapi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge/api"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["api"] = api.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								gateway/bridgemap/bdiscord.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								gateway/bridgemap/bdiscord.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					// +build !nodiscord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bdiscord "github.com/42wim/matterbridge/bridge/discord"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["discord"] = bdiscord.New
 | 
				
			||||||
 | 
						UserTypingSupport["discord"] = struct{}{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bgitter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bgitter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !nogitter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bgitter "github.com/42wim/matterbridge/bridge/gitter"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["gitter"] = bgitter.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								gateway/bridgemap/bharmony.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								gateway/bridgemap/bharmony.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					//go:build !noharmony
 | 
				
			||||||
 | 
					// +build !noharmony
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bharmony "github.com/42wim/matterbridge/bridge/harmony"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["harmony"] = bharmony.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/birc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/birc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !noirc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						birc "github.com/42wim/matterbridge/bridge/irc"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["irc"] = birc.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bkeybase.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bkeybase.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !nokeybase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bkeybase "github.com/42wim/matterbridge/bridge/keybase"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["keybase"] = bkeybase.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmatrix.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bmatrix.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !nomatrix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bmatrix "github.com/42wim/matterbridge/bridge/matrix"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["matrix"] = bmatrix.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmattermost.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bmattermost.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !nomattermost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["mattermost"] = bmattermost.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmsteams.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bmsteams.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !nomsteams
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bmsteams "github.com/42wim/matterbridge/bridge/msteams"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["msteams"] = bmsteams.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmumble.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bmumble.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !nomumble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						bmumble "github.com/42wim/matterbridge/bridge/mumble"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["mumble"] = bmumble.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bnctalk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bnctalk.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					// +build !nonctalk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						btalk "github.com/42wim/matterbridge/bridge/nctalk"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						FullMap["nctalk"] = btalk.New
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								gateway/bridgemap/bridgemap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								gateway/bridgemap/bridgemap.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					package bridgemap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/42wim/matterbridge/bridge"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						FullMap           = map[string]bridge.Factory{}
 | 
				
			||||||
 | 
						UserTypingSupport = map[string]struct{}{}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user