aboutsummaryrefslogtreecommitdiff
path: root/weechat/lua
diff options
context:
space:
mode:
Diffstat (limited to 'weechat/lua')
l---------weechat/lua/autoload/emoji.lua1
l---------weechat/lua/autoload/matrix.lua1
-rw-r--r--weechat/lua/emoji.lua207
-rw-r--r--weechat/lua/matrix.lua3354
4 files changed, 3563 insertions, 0 deletions
diff --git a/weechat/lua/autoload/emoji.lua b/weechat/lua/autoload/emoji.lua
new file mode 120000
index 0000000..91557c7
--- /dev/null
+++ b/weechat/lua/autoload/emoji.lua
@@ -0,0 +1 @@
+../emoji.lua \ No newline at end of file
diff --git a/weechat/lua/autoload/matrix.lua b/weechat/lua/autoload/matrix.lua
new file mode 120000
index 0000000..d91a9aa
--- /dev/null
+++ b/weechat/lua/autoload/matrix.lua
@@ -0,0 +1 @@
+/home/neodarz/.weechat/lua/matrix.lua \ No newline at end of file
diff --git a/weechat/lua/emoji.lua b/weechat/lua/emoji.lua
new file mode 100644
index 0000000..75d4b44
--- /dev/null
+++ b/weechat/lua/emoji.lua
@@ -0,0 +1,207 @@
+-- Copyright 2016 xt <xt@bash.no>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+--
+
+
+--[[
+
+
+Emoji output/input shortcode helper for WeeChat.
+
+Usage:
+ Type :emojiname: from http://www.emoji-cheat-sheet.com/ or :partial match<tab>
+
+
+ Changelog:
+ version 3, 2016-11-04, xt
+ * add a slack specific shortcode
+ version 2, 2016-10-31, xt
+ * some cleanup
+ * ability replace incoming
+ version 1, 2016-02-29, xt
+ * initial version
+
+
+EMOJI CONVERSION SCRIPT:
+import json
+
+e = json.loads(open('emoji.json').read())
+print "emoji = {"
+emojis = []
+for k, v in e.items():
+ shortname = v['shortname'].strip(':')
+ ubytes = []
+ cps = v['unicode'].split('-')
+ for c in cps:
+ ubytes.append(unichr(int(c, 16)))
+ if shortname in ['end', 'repeat']:
+ shortname = '["%s"]' %shortname
+ elif '-' in shortname:
+ shortname = '["%s"]' %shortname
+ elif shortname[0].isdigit():
+ shortname = '["%s"]' %shortname
+ else:
+ try:
+ shortname = int(shortname)
+ shortname = '["%s"]' %shortname
+ except:
+ pass
+ emojis.append((shortname+'="'+u"".join(ubytes)+'",').encode('UTF-8'))
+print "".join(emojis)
+print "}"
+--]]
+
+local SCRIPT_NAME = "emoji"
+local SCRIPT_AUTHOR = "xt <xt@bash.no>"
+local SCRIPT_VERSION = "3"
+local SCRIPT_LICENSE = "GPL3"
+local SCRIPT_DESC = "Emoji output helper"
+
+
+-- luacheck: ignore weechat
+local w = weechat
+local emoji = {
+four="4โƒฃ",kiss_ww="๐Ÿ‘ฉโค๐Ÿ’‹๐Ÿ‘ฉ",maple_leaf="๐Ÿ",waxing_gibbous_moon="๐ŸŒ”",bike="๐Ÿšฒ",recycle="โ™ป",family_mwgb="๐Ÿ‘จ๐Ÿ‘ฉ๐Ÿ‘ง๐Ÿ‘ฆ",flag_dk="๐Ÿ‡ฉ๐Ÿ‡ฐ",thought_balloon="๐Ÿ’ญ",oncoming_automobile="๐Ÿš˜",guardsman_tone5="๐Ÿ’‚๐Ÿฟ",tickets="๐ŸŽŸ",school="๐Ÿซ",house_abandoned="๐Ÿš",blue_book="๐Ÿ“˜",video_game="๐ŸŽฎ",triumph="๐Ÿ˜ค",suspension_railway="๐ŸšŸ",umbrella="โ˜”",levitate="๐Ÿ•ด",cactus="๐ŸŒต",monorail="๐Ÿš",stars="๐ŸŒ ",new="๐Ÿ†•",herb="๐ŸŒฟ",pouting_cat="๐Ÿ˜พ",blue_heart="๐Ÿ’™",["100"]="๐Ÿ’ฏ",leaves="๐Ÿƒ",family_mwbb="๐Ÿ‘จ๐Ÿ‘ฉ๐Ÿ‘ฆ๐Ÿ‘ฆ",information_desk_person_tone2="๐Ÿ’๐Ÿผ",dragon_face="๐Ÿฒ",track_next="โญ",cloud_snow="๐ŸŒจ",flag_jp="๐Ÿ‡ฏ๐Ÿ‡ต",children_crossing="๐Ÿšธ",information_desk_person_tone1="๐Ÿ’๐Ÿป",arrow_up_down="โ†•",mount_fuji="๐Ÿ—ป",massage_tone1="๐Ÿ’†๐Ÿป",flag_mq="๐Ÿ‡ฒ๐Ÿ‡ถ",massage_tone3="๐Ÿ’†๐Ÿฝ",massage_tone2="๐Ÿ’†๐Ÿผ",massage_tone5="๐Ÿ’†๐Ÿฟ",flag_je="๐Ÿ‡ฏ๐Ÿ‡ช",flag_jm="๐Ÿ‡ฏ๐Ÿ‡ฒ",flag_jo="๐Ÿ‡ฏ๐Ÿ‡ด",red_car="๐Ÿš—",hospital="๐Ÿฅ",red_circle="๐Ÿ”ด",princess="๐Ÿ‘ธ",tm="โ„ข",curly_loop="โžฐ",boy_tone5="๐Ÿ‘ฆ๐Ÿฟ",pouch="๐Ÿ‘",boy_tone3="๐Ÿ‘ฆ๐Ÿฝ",boy_tone1="๐Ÿ‘ฆ๐Ÿป",izakaya_lantern="๐Ÿฎ",fist_tone5="โœŠ๐Ÿฟ",fist_tone4="โœŠ๐Ÿพ",fist_tone1="โœŠ๐Ÿป",fist_tone3="โœŠ๐Ÿฝ",fist_tone2="โœŠ๐Ÿผ",arrow_lower_left="โ†™",game_die="๐ŸŽฒ",pushpin="๐Ÿ“Œ",dividers="๐Ÿ—‚",dolphin="๐Ÿฌ",night_with_stars="๐ŸŒƒ",cruise_ship="๐Ÿ›ณ",white_medium_small_square="โ—ฝ",kissing_closed_eyes="๐Ÿ˜š",earth_americas="๐ŸŒŽ",["end"]="๐Ÿ”š",mouse="๐Ÿญ",rewind="โช",beach="๐Ÿ–",pizza="๐Ÿ•",briefcase="๐Ÿ’ผ",customs="๐Ÿ›ƒ",heartpulse="๐Ÿ’—",sparkler="๐ŸŽ‡",sparkles="โœจ",hand_splayed_tone1="๐Ÿ–๐Ÿป",snowman2="โ˜ƒ",tulip="๐ŸŒท",speaking_head="๐Ÿ—ฃ",ambulance="๐Ÿš‘",office="๐Ÿข",clapper="๐ŸŽฌ",keyboard="โŒจ",japan="๐Ÿ—พ",post_office="๐Ÿฃ",dizzy_face="๐Ÿ˜ต",imp="๐Ÿ‘ฟ",flag_ve="๐Ÿ‡ป๐Ÿ‡ช",coffee="โ˜•",flag_vg="๐Ÿ‡ป๐Ÿ‡ฌ",flag_va="๐Ÿ‡ป๐Ÿ‡ฆ",flag_vc="๐Ÿ‡ป๐Ÿ‡จ",flag_vn="๐Ÿ‡ป๐Ÿ‡ณ",flag_vi="๐Ÿ‡ป๐Ÿ‡ฎ",open_mouth="๐Ÿ˜ฎ",flag_vu="๐Ÿ‡ป๐Ÿ‡บ",page_with_curl="๐Ÿ“ƒ",bank="๐Ÿฆ",bread="๐Ÿž",oncoming_police_car="๐Ÿš”",capricorn="โ™‘",point_left="๐Ÿ‘ˆ",tokyo_tower="๐Ÿ—ผ",fishing_pole_and_fish="๐ŸŽฃ",thumbsdown="๐Ÿ‘Ž",telescope="๐Ÿ”ญ",spider="๐Ÿ•ท",u7121="๐Ÿˆš",camera_with_flash="๐Ÿ“ธ",lifter="๐Ÿ‹",sweet_potato="๐Ÿ ",lock_with_ink_pen="๐Ÿ”",ok_woman_tone2="๐Ÿ™†๐Ÿผ",ok_woman_tone3="๐Ÿ™†๐Ÿฝ",smirk="๐Ÿ˜",baggage_claim="๐Ÿ›„",cherry_blossom="๐ŸŒธ",sparkle="โ‡",zap="โšก",construction_site="๐Ÿ—",dancers="๐Ÿ‘ฏ",flower_playing_cards="๐ŸŽด",hatching_chick="๐Ÿฃ",free="๐Ÿ†“",bullettrain_side="๐Ÿš„",poultry_leg="๐Ÿ—",grapes="๐Ÿ‡",smirk_cat="๐Ÿ˜ผ",lollipop="๐Ÿญ",water_buffalo="๐Ÿƒ",black_medium_small_square="โ—พ",atm="๐Ÿง",gift_heart="๐Ÿ’",older_woman_tone5="๐Ÿ‘ต๐Ÿฟ",older_woman_tone4="๐Ÿ‘ต๐Ÿพ",older_woman_tone1="๐Ÿ‘ต๐Ÿป",older_woman_tone3="๐Ÿ‘ต๐Ÿฝ",older_woman_tone2="๐Ÿ‘ต๐Ÿผ",scissors="โœ‚",woman_tone2="๐Ÿ‘ฉ๐Ÿผ",basketball="๐Ÿ€",hammer_pick="โš’",top="๐Ÿ”",clock630="๐Ÿ•ก",raising_hand_tone5="๐Ÿ™‹๐Ÿฟ",railway_track="๐Ÿ›ค",nail_care="๐Ÿ’…",crossed_flags="๐ŸŽŒ",minibus="๐Ÿš",white_sun_cloud="๐ŸŒฅ",shower="๐Ÿšฟ",smile_cat="๐Ÿ˜ธ",dog2="๐Ÿ•",loud_sound="๐Ÿ”Š",kaaba="๐Ÿ•‹",runner="๐Ÿƒ",ram="๐Ÿ",writing_hand="โœ",rat="๐Ÿ€",rice_scene="๐ŸŽ‘",milky_way="๐ŸŒŒ",vulcan_tone5="๐Ÿ––๐Ÿฟ",necktie="๐Ÿ‘”",kissing_cat="๐Ÿ˜ฝ",snowflake="โ„",paintbrush="๐Ÿ–Œ",crystal_ball="๐Ÿ”ฎ",mountain_bicyclist_tone4="๐Ÿšต๐Ÿพ",mountain_bicyclist_tone3="๐Ÿšต๐Ÿฝ",mountain_bicyclist_tone2="๐Ÿšต๐Ÿผ",mountain_bicyclist_tone1="๐Ÿšต๐Ÿป",koko="๐Ÿˆ",flag_it="๐Ÿ‡ฎ๐Ÿ‡น",flag_iq="๐Ÿ‡ฎ๐Ÿ‡ถ",flag_is="๐Ÿ‡ฎ๐Ÿ‡ธ",flag_ir="๐Ÿ‡ฎ๐Ÿ‡ท",flag_im="๐Ÿ‡ฎ๐Ÿ‡ฒ",flag_il="๐Ÿ‡ฎ๐Ÿ‡ฑ",flag_io="๐Ÿ‡ฎ๐Ÿ‡ด",flag_in="๐Ÿ‡ฎ๐Ÿ‡ณ",flag_ie="๐Ÿ‡ฎ๐Ÿ‡ช",flag_id="๐Ÿ‡ฎ๐Ÿ‡ฉ",flag_ic="๐Ÿ‡ฎ๐Ÿ‡จ",ballot_box_with_check="โ˜‘",mountain_bicyclist_tone5="๐Ÿšต๐Ÿฟ",metal="๐Ÿค˜",dog="๐Ÿถ",pineapple="๐Ÿ",no_good_tone3="๐Ÿ™…๐Ÿฝ",no_good_tone2="๐Ÿ™…๐Ÿผ",no_good_tone1="๐Ÿ™…๐Ÿป",scream="๐Ÿ˜ฑ",no_good_tone5="๐Ÿ™…๐Ÿฟ",no_good_tone4="๐Ÿ™…๐Ÿพ",flag_ua="๐Ÿ‡บ๐Ÿ‡ฆ",bomb="๐Ÿ’ฃ",flag_ug="๐Ÿ‡บ๐Ÿ‡ฌ",flag_um="๐Ÿ‡บ๐Ÿ‡ฒ",flag_us="๐Ÿ‡บ๐Ÿ‡ธ",construction_worker_tone1="๐Ÿ‘ท๐Ÿป",radio="๐Ÿ“ป",flag_uy="๐Ÿ‡บ๐Ÿ‡พ",flag_uz="๐Ÿ‡บ๐Ÿ‡ฟ",person_with_blond_hair_tone1="๐Ÿ‘ฑ๐Ÿป",cupid="๐Ÿ’˜",mens="๐Ÿšน",rice="๐Ÿš",point_right_tone1="๐Ÿ‘‰๐Ÿป",point_right_tone3="๐Ÿ‘‰๐Ÿฝ",point_right_tone2="๐Ÿ‘‰๐Ÿผ",sunglasses="๐Ÿ˜Ž",point_right_tone4="๐Ÿ‘‰๐Ÿพ",watch="โŒš",frowning="๐Ÿ˜ฆ",watermelon="๐Ÿ‰",wedding="๐Ÿ’’",person_frowning_tone4="๐Ÿ™๐Ÿพ",person_frowning_tone5="๐Ÿ™๐Ÿฟ",person_frowning_tone2="๐Ÿ™๐Ÿผ",person_frowning_tone3="๐Ÿ™๐Ÿฝ",person_frowning_tone1="๐Ÿ™๐Ÿป",flag_gw="๐Ÿ‡ฌ๐Ÿ‡ผ",flag_gu="๐Ÿ‡ฌ๐Ÿ‡บ",flag_gt="๐Ÿ‡ฌ๐Ÿ‡น",flag_gs="๐Ÿ‡ฌ๐Ÿ‡ธ",flag_gr="๐Ÿ‡ฌ๐Ÿ‡ท",flag_gq="๐Ÿ‡ฌ๐Ÿ‡ถ",flag_gp="๐Ÿ‡ฌ๐Ÿ‡ต",flag_gy="๐Ÿ‡ฌ๐Ÿ‡พ",flag_gg="๐Ÿ‡ฌ๐Ÿ‡ฌ",flag_gf="๐Ÿ‡ฌ๐Ÿ‡ซ",microscope="๐Ÿ”ฌ",flag_gd="๐Ÿ‡ฌ๐Ÿ‡ฉ",flag_gb="๐Ÿ‡ฌ๐Ÿ‡ง",flag_ga="๐Ÿ‡ฌ๐Ÿ‡ฆ",flag_gn="๐Ÿ‡ฌ๐Ÿ‡ณ",flag_gm="๐Ÿ‡ฌ๐Ÿ‡ฒ",flag_gl="๐Ÿ‡ฌ๐Ÿ‡ฑ",japanese_ogre="๐Ÿ‘น",flag_gi="๐Ÿ‡ฌ๐Ÿ‡ฎ",flag_gh="๐Ÿ‡ฌ๐Ÿ‡ญ",man_with_turban="๐Ÿ‘ณ",star_and_crescent="โ˜ช",writing_hand_tone3="โœ๐Ÿฝ",dromedary_camel="๐Ÿช",hash="#โƒฃ",hammer="๐Ÿ”จ",hourglass="โŒ›",postbox="๐Ÿ“ฎ",writing_hand_tone5="โœ๐Ÿฟ",writing_hand_tone4="โœ๐Ÿพ",wc="๐Ÿšพ",aquarius="โ™’",couple_with_heart="๐Ÿ’‘",ok_woman="๐Ÿ™†",raised_hands_tone4="๐Ÿ™Œ๐Ÿพ",cop="๐Ÿ‘ฎ",raised_hands_tone1="๐Ÿ™Œ๐Ÿป",cow="๐Ÿฎ",raised_hands_tone3="๐Ÿ™Œ๐Ÿฝ",white_large_square="โฌœ",pig_nose="๐Ÿฝ",ice_skate="โ›ธ",hotsprings="โ™จ",tone5="๐Ÿฟ",three="3โƒฃ",beer="๐Ÿบ",stadium="๐ŸŸ",airplane_departure="๐Ÿ›ซ",heavy_division_sign="โž—",flag_black="๐Ÿด",mushroom="๐Ÿ„",record_button="โบ",vulcan="๐Ÿ––",dash="๐Ÿ’จ",wind_chime="๐ŸŽ",anchor="โš“",seven="7โƒฃ",flag_hr="๐Ÿ‡ญ๐Ÿ‡ท",roller_coaster="๐ŸŽข",pen_ballpoint="๐Ÿ–Š",sushi="๐Ÿฃ",flag_ht="๐Ÿ‡ญ๐Ÿ‡น",flag_hu="๐Ÿ‡ญ๐Ÿ‡บ",flag_hk="๐Ÿ‡ญ๐Ÿ‡ฐ",dizzy="๐Ÿ’ซ",flag_hn="๐Ÿ‡ญ๐Ÿ‡ณ",flag_hm="๐Ÿ‡ญ๐Ÿ‡ฒ",arrow_forward="โ–ถ",violin="๐ŸŽป",orthodox_cross="โ˜ฆ",id="๐Ÿ†”",heart_decoration="๐Ÿ’Ÿ",first_quarter_moon="๐ŸŒ“",satellite="๐Ÿ“ก",tone3="๐Ÿฝ",christmas_tree="๐ŸŽ„",unicorn="๐Ÿฆ„",broken_heart="๐Ÿ’”",ocean="๐ŸŒŠ",hearts="โ™ฅ",snowman="โ›„",person_with_blond_hair_tone4="๐Ÿ‘ฑ๐Ÿพ",person_with_blond_hair_tone5="๐Ÿ‘ฑ๐Ÿฟ",person_with_blond_hair_tone2="๐Ÿ‘ฑ๐Ÿผ",person_with_blond_hair_tone3="๐Ÿ‘ฑ๐Ÿฝ",yen="๐Ÿ’ด",straight_ruler="๐Ÿ“",sleepy="๐Ÿ˜ช",green_apple="๐Ÿ",white_medium_square="โ—ป",flag_fr="๐Ÿ‡ซ๐Ÿ‡ท",grey_exclamation="โ•",innocent="๐Ÿ˜‡",flag_fm="๐Ÿ‡ซ๐Ÿ‡ฒ",flag_fo="๐Ÿ‡ซ๐Ÿ‡ด",flag_fi="๐Ÿ‡ซ๐Ÿ‡ฎ",flag_fj="๐Ÿ‡ซ๐Ÿ‡ฏ",flag_fk="๐Ÿ‡ซ๐Ÿ‡ฐ",menorah="๐Ÿ•Ž",yin_yang="โ˜ฏ",clock130="๐Ÿ•œ",gift="๐ŸŽ",prayer_beads="๐Ÿ“ฟ",stuck_out_tongue="๐Ÿ˜›",om_symbol="๐Ÿ•‰",city_dusk="๐ŸŒ†",massage_tone4="๐Ÿ’†๐Ÿพ",couple_ww="๐Ÿ‘ฉโค๐Ÿ‘ฉ",crown="๐Ÿ‘‘",sparkling_heart="๐Ÿ’–",clubs="โ™ฃ",person_with_pouting_face="๐Ÿ™Ž",newspaper2="๐Ÿ—ž",fog="๐ŸŒซ",dango="๐Ÿก",large_orange_diamond="๐Ÿ”ถ",flag_tn="๐Ÿ‡น๐Ÿ‡ณ",flag_to="๐Ÿ‡น๐Ÿ‡ด",point_up="โ˜",flag_tm="๐Ÿ‡น๐Ÿ‡ฒ",flag_tj="๐Ÿ‡น๐Ÿ‡ฏ",flag_tk="๐Ÿ‡น๐Ÿ‡ฐ",flag_th="๐Ÿ‡น๐Ÿ‡ญ",flag_tf="๐Ÿ‡น๐Ÿ‡ซ",flag_tg="๐Ÿ‡น๐Ÿ‡ฌ",corn="๐ŸŒฝ",flag_tc="๐Ÿ‡น๐Ÿ‡จ",flag_ta="๐Ÿ‡น๐Ÿ‡ฆ",flag_tz="๐Ÿ‡น๐Ÿ‡ฟ",flag_tv="๐Ÿ‡น๐Ÿ‡ป",flag_tw="๐Ÿ‡น๐Ÿ‡ผ",flag_tt="๐Ÿ‡น๐Ÿ‡น",flag_tr="๐Ÿ‡น๐Ÿ‡ท",eight_spoked_asterisk="โœณ",trophy="๐Ÿ†",black_small_square="โ–ช",o="โญ•",no_bell="๐Ÿ”•",curry="๐Ÿ›",alembic="โš—",sob="๐Ÿ˜ญ",waxing_crescent_moon="๐ŸŒ’",tiger2="๐Ÿ…",two="2โƒฃ",sos="๐Ÿ†˜",compression="๐Ÿ—œ",heavy_multiplication_x="โœ–",tennis="๐ŸŽพ",fireworks="๐ŸŽ†",skull_crossbones="โ˜ ",astonished="๐Ÿ˜ฒ",congratulations="ใŠ—",grey_question="โ”",arrow_upper_left="โ†–",arrow_double_up="โซ",triangular_flag_on_post="๐Ÿšฉ",gemini="โ™Š",door="๐Ÿšช",ship="๐Ÿšข",point_down_tone3="๐Ÿ‘‡๐Ÿฝ",point_down_tone4="๐Ÿ‘‡๐Ÿพ",point_down_tone5="๐Ÿ‘‡๐Ÿฟ",movie_camera="๐ŸŽฅ",ng="๐Ÿ†–",couple_mm="๐Ÿ‘จโค๐Ÿ‘จ",football="๐Ÿˆ",asterisk="*โƒฃ",taurus="โ™‰",articulated_lorry="๐Ÿš›",police_car="๐Ÿš“",flushed="๐Ÿ˜ณ",spades="โ™ ",cloud_lightning="๐ŸŒฉ",wine_glass="๐Ÿท",clock830="๐Ÿ•ฃ",punch_tone2="๐Ÿ‘Š๐Ÿผ",punch_tone3="๐Ÿ‘Š๐Ÿฝ",punch_tone1="๐Ÿ‘Š๐Ÿป",department_store="๐Ÿฌ",punch_tone4="๐Ÿ‘Š๐Ÿพ",punch_tone5="๐Ÿ‘Š๐Ÿฟ",crocodile="๐ŸŠ",white_square_button="๐Ÿ”ณ",hole="๐Ÿ•ณ",boy_tone2="๐Ÿ‘ฆ๐Ÿผ",mountain_cableway="๐Ÿš ",melon="๐Ÿˆ",persevere="๐Ÿ˜ฃ",trident="๐Ÿ”ฑ",head_bandage="๐Ÿค•",u7a7a="๐Ÿˆณ",cool="๐Ÿ†’",high_brightness="๐Ÿ”†",deciduous_tree="๐ŸŒณ",white_flower="๐Ÿ’ฎ",gun="๐Ÿ”ซ",flag_sk="๐Ÿ‡ธ๐Ÿ‡ฐ",flag_sj="๐Ÿ‡ธ๐Ÿ‡ฏ",flag_si="๐Ÿ‡ธ๐Ÿ‡ฎ",flag_sh="๐Ÿ‡ธ๐Ÿ‡ญ",flag_so="๐Ÿ‡ธ๐Ÿ‡ด",flag_sn="๐Ÿ‡ธ๐Ÿ‡ณ",flag_sm="๐Ÿ‡ธ๐Ÿ‡ฒ",flag_sl="๐Ÿ‡ธ๐Ÿ‡ฑ",flag_sc="๐Ÿ‡ธ๐Ÿ‡จ",flag_sb="๐Ÿ‡ธ๐Ÿ‡ง",flag_sa="๐Ÿ‡ธ๐Ÿ‡ฆ",flag_sg="๐Ÿ‡ธ๐Ÿ‡ฌ",flag_tl="๐Ÿ‡น๐Ÿ‡ฑ",flag_se="๐Ÿ‡ธ๐Ÿ‡ช",arrow_left="โฌ…",flag_sz="๐Ÿ‡ธ๐Ÿ‡ฟ",flag_sy="๐Ÿ‡ธ๐Ÿ‡พ",small_orange_diamond="๐Ÿ”ธ",flag_ss="๐Ÿ‡ธ๐Ÿ‡ธ",flag_sr="๐Ÿ‡ธ๐Ÿ‡ท",flag_sv="๐Ÿ‡ธ๐Ÿ‡ป",flag_st="๐Ÿ‡ธ๐Ÿ‡น",file_folder="๐Ÿ“",flag_td="๐Ÿ‡น๐Ÿ‡ฉ",["1234"]="๐Ÿ”ข",smiling_imp="๐Ÿ˜ˆ",surfer_tone2="๐Ÿ„๐Ÿผ",surfer_tone3="๐Ÿ„๐Ÿฝ",surfer_tone4="๐Ÿ„๐Ÿพ",surfer_tone5="๐Ÿ„๐Ÿฟ",amphora="๐Ÿบ",baseball="โšพ",boy="๐Ÿ‘ฆ",flag_es="๐Ÿ‡ช๐Ÿ‡ธ",raised_hands="๐Ÿ™Œ",flag_eu="๐Ÿ‡ช๐Ÿ‡บ",flag_et="๐Ÿ‡ช๐Ÿ‡น",heavy_plus_sign="โž•",bow="๐Ÿ™‡",flag_ea="๐Ÿ‡ช๐Ÿ‡ฆ",flag_ec="๐Ÿ‡ช๐Ÿ‡จ",flag_ee="๐Ÿ‡ช๐Ÿ‡ช",light_rail="๐Ÿšˆ",flag_eg="๐Ÿ‡ช๐Ÿ‡ฌ",flag_eh="๐Ÿ‡ช๐Ÿ‡ญ",massage="๐Ÿ’†",man_with_gua_pi_mao_tone4="๐Ÿ‘ฒ๐Ÿพ",man_with_gua_pi_mao_tone3="๐Ÿ‘ฒ๐Ÿฝ",outbox_tray="๐Ÿ“ค",clock330="๐Ÿ•ž",projector="๐Ÿ“ฝ",sake="๐Ÿถ",confounded="๐Ÿ˜–",angry="๐Ÿ˜ ",iphone="๐Ÿ“ฑ",sweat_smile="๐Ÿ˜…",aries="โ™ˆ",ear_of_rice="๐ŸŒพ",mouse2="๐Ÿ",bicyclist_tone4="๐Ÿšด๐Ÿพ",bicyclist_tone5="๐Ÿšด๐Ÿฟ",guardsman="๐Ÿ’‚",bicyclist_tone1="๐Ÿšด๐Ÿป",bicyclist_tone2="๐Ÿšด๐Ÿผ",bicyclist_tone3="๐Ÿšด๐Ÿฝ",envelope="โœ‰",money_with_wings="๐Ÿ’ธ",beers="๐Ÿป",heart_exclamation="โฃ",notepad_spiral="๐Ÿ—’",cat="๐Ÿฑ",running_shirt_with_sash="๐ŸŽฝ",ferry="โ›ด",spy="๐Ÿ•ต",chart_with_upwards_trend="๐Ÿ“ˆ",green_heart="๐Ÿ’š",confused="๐Ÿ˜•",angel_tone4="๐Ÿ‘ผ๐Ÿพ",scorpius="โ™",sailboat="โ›ต",elephant="๐Ÿ˜",map="๐Ÿ—บ",disappointed_relieved="๐Ÿ˜ฅ",flag_xk="๐Ÿ‡ฝ๐Ÿ‡ฐ",motorway="๐Ÿ›ฃ",sun_with_face="๐ŸŒž",birthday="๐ŸŽ‚",mag="๐Ÿ”",date="๐Ÿ“…",dove="๐Ÿ•Š",man="๐Ÿ‘จ",octopus="๐Ÿ™",wheelchair="โ™ฟ",truck="๐Ÿšš",sa="๐Ÿˆ‚",shield="๐Ÿ›ก",haircut="๐Ÿ’‡",last_quarter_moon_with_face="๐ŸŒœ",rosette="๐Ÿต",currency_exchange="๐Ÿ’ฑ",mailbox_with_no_mail="๐Ÿ“ญ",bath="๐Ÿ›€",clock930="๐Ÿ•ค",bowling="๐ŸŽณ",turtle="๐Ÿข",pause_button="โธ",construction_worker="๐Ÿ‘ท",unlock="๐Ÿ”“",anger_right="๐Ÿ—ฏ",beetle="๐Ÿž",girl="๐Ÿ‘ง",sunrise="๐ŸŒ…",exclamation="โ—",flag_dz="๐Ÿ‡ฉ๐Ÿ‡ฟ",family_mmgg="๐Ÿ‘จ๐Ÿ‘จ๐Ÿ‘ง๐Ÿ‘ง",factory="๐Ÿญ",flag_do="๐Ÿ‡ฉ๐Ÿ‡ด",flag_dm="๐Ÿ‡ฉ๐Ÿ‡ฒ",flag_dj="๐Ÿ‡ฉ๐Ÿ‡ฏ",mouse_three_button="๐Ÿ–ฑ",flag_dg="๐Ÿ‡ฉ๐Ÿ‡ฌ",flag_de="๐Ÿ‡ฉ๐Ÿ‡ช",star_of_david="โœก",reminder_ribbon="๐ŸŽ—",grimacing="๐Ÿ˜ฌ",thumbsup_tone3="๐Ÿ‘๐Ÿฝ",thumbsup_tone2="๐Ÿ‘๐Ÿผ",thumbsup_tone1="๐Ÿ‘๐Ÿป",musical_note="๐ŸŽต",thumbsup_tone5="๐Ÿ‘๐Ÿฟ",thumbsup_tone4="๐Ÿ‘๐Ÿพ",high_heel="๐Ÿ‘ ",green_book="๐Ÿ“—",headphones="๐ŸŽง",flag_aw="๐Ÿ‡ฆ๐Ÿ‡ผ",stop_button="โน",yum="๐Ÿ˜‹",flag_aq="๐Ÿ‡ฆ๐Ÿ‡ถ",warning="โš ",cheese="๐Ÿง€",ophiuchus="โ›Ž",revolving_hearts="๐Ÿ’ž",one="1โƒฃ",ring="๐Ÿ’",point_right="๐Ÿ‘‰",sheep="๐Ÿ‘",bookmark="๐Ÿ”–",spider_web="๐Ÿ•ธ",eyes="๐Ÿ‘€",flag_ro="๐Ÿ‡ท๐Ÿ‡ด",flag_re="๐Ÿ‡ท๐Ÿ‡ช",flag_rs="๐Ÿ‡ท๐Ÿ‡ธ",sweat_drops="๐Ÿ’ฆ",flag_ru="๐Ÿ‡ท๐Ÿ‡บ",flag_rw="๐Ÿ‡ท๐Ÿ‡ผ",middle_finger="๐Ÿ–•",race_car="๐ŸŽ",evergreen_tree="๐ŸŒฒ",biohazard="โ˜ฃ",girl_tone3="๐Ÿ‘ง๐Ÿฝ",scream_cat="๐Ÿ™€",computer="๐Ÿ’ป",hourglass_flowing_sand="โณ",flag_lb="๐Ÿ‡ฑ๐Ÿ‡ง",tophat="๐ŸŽฉ",clock1230="๐Ÿ•ง",tractor="๐Ÿšœ",u6709="๐Ÿˆถ",u6708="๐Ÿˆท",crying_cat_face="๐Ÿ˜ฟ",angel="๐Ÿ‘ผ",ant="๐Ÿœ",information_desk_person="๐Ÿ’",anger="๐Ÿ’ข",mailbox_with_mail="๐Ÿ“ฌ",pencil2="โœ",wink="๐Ÿ˜‰",thermometer="๐ŸŒก",relaxed="โ˜บ",printer="๐Ÿ–จ",credit_card="๐Ÿ’ณ",checkered_flag="๐Ÿ",family_mmg="๐Ÿ‘จ๐Ÿ‘จ๐Ÿ‘ง",pager="๐Ÿ“Ÿ",family_mmb="๐Ÿ‘จ๐Ÿ‘จ๐Ÿ‘ฆ",radioactive="โ˜ข",fried_shrimp="๐Ÿค",link="๐Ÿ”—",walking="๐Ÿšถ",city_sunset="๐ŸŒ‡",shopping_bags="๐Ÿ›",hockey="๐Ÿ’",arrow_up="โฌ†",gem="๐Ÿ’Ž",negative_squared_cross_mark="โŽ",worried="๐Ÿ˜Ÿ",walking_tone5="๐Ÿšถ๐Ÿฟ",walking_tone1="๐Ÿšถ๐Ÿป",hear_no_evil="๐Ÿ™‰",convenience_store="๐Ÿช",seat="๐Ÿ’บ",girl_tone1="๐Ÿ‘ง๐Ÿป",cloud_rain="๐ŸŒง",girl_tone2="๐Ÿ‘ง๐Ÿผ",girl_tone5="๐Ÿ‘ง๐Ÿฟ",girl_tone4="๐Ÿ‘ง๐Ÿพ",parking="๐Ÿ…ฟ",pisces="โ™“",calendar="๐Ÿ“†",loudspeaker="๐Ÿ“ข",camping="๐Ÿ•",bicyclist="๐Ÿšด",label="๐Ÿท",diamonds="โ™ฆ",older_man_tone1="๐Ÿ‘ด๐Ÿป",older_man_tone3="๐Ÿ‘ด๐Ÿฝ",older_man_tone2="๐Ÿ‘ด๐Ÿผ",older_man_tone5="๐Ÿ‘ด๐Ÿฟ",older_man_tone4="๐Ÿ‘ด๐Ÿพ",microphone2="๐ŸŽ™",raising_hand="๐Ÿ™‹",hot_pepper="๐ŸŒถ",guitar="๐ŸŽธ",tropical_drink="๐Ÿน",upside_down="๐Ÿ™ƒ",restroom="๐Ÿšป",pen_fountain="๐Ÿ–‹",comet="โ˜„",cancer="โ™‹",jeans="๐Ÿ‘–",flag_qa="๐Ÿ‡ถ๐Ÿ‡ฆ",boar="๐Ÿ—",turkey="๐Ÿฆƒ",person_with_blond_hair="๐Ÿ‘ฑ",oden="๐Ÿข",stuck_out_tongue_closed_eyes="๐Ÿ˜",helicopter="๐Ÿš",control_knobs="๐ŸŽ›",performing_arts="๐ŸŽญ",tiger="๐Ÿฏ",foggy="๐ŸŒ",sound="๐Ÿ”‰",flag_cz="๐Ÿ‡จ๐Ÿ‡ฟ",flag_cy="๐Ÿ‡จ๐Ÿ‡พ",flag_cx="๐Ÿ‡จ๐Ÿ‡ฝ",speech_balloon="๐Ÿ’ฌ",seedling="๐ŸŒฑ",flag_cr="๐Ÿ‡จ๐Ÿ‡ท",envelope_with_arrow="๐Ÿ“ฉ",flag_cp="๐Ÿ‡จ๐Ÿ‡ต",flag_cw="๐Ÿ‡จ๐Ÿ‡ผ",flag_cv="๐Ÿ‡จ๐Ÿ‡ป",flag_cu="๐Ÿ‡จ๐Ÿ‡บ",flag_ck="๐Ÿ‡จ๐Ÿ‡ฐ",flag_ci="๐Ÿ‡จ๐Ÿ‡ฎ",flag_ch="๐Ÿ‡จ๐Ÿ‡ญ",flag_co="๐Ÿ‡จ๐Ÿ‡ด",flag_cn="๐Ÿ‡จ๐Ÿ‡ณ",flag_cm="๐Ÿ‡จ๐Ÿ‡ฒ",u5408="๐Ÿˆด",flag_cc="๐Ÿ‡จ๐Ÿ‡จ",flag_ca="๐Ÿ‡จ๐Ÿ‡ฆ",flag_cg="๐Ÿ‡จ๐Ÿ‡ฌ",flag_cf="๐Ÿ‡จ๐Ÿ‡ซ",flag_cd="๐Ÿ‡จ๐Ÿ‡ฉ",purse="๐Ÿ‘›",telephone="โ˜Ž",sleeping="๐Ÿ˜ด",point_down_tone1="๐Ÿ‘‡๐Ÿป",frowning2="โ˜น",point_down_tone2="๐Ÿ‘‡๐Ÿผ",muscle_tone4="๐Ÿ’ช๐Ÿพ",muscle_tone5="๐Ÿ’ช๐Ÿฟ",synagogue="๐Ÿ•",muscle_tone1="๐Ÿ’ช๐Ÿป",muscle_tone2="๐Ÿ’ช๐Ÿผ",muscle_tone3="๐Ÿ’ช๐Ÿฝ",clap_tone5="๐Ÿ‘๐Ÿฟ",clap_tone4="๐Ÿ‘๐Ÿพ",clap_tone1="๐Ÿ‘๐Ÿป",train2="๐Ÿš†",clap_tone2="๐Ÿ‘๐Ÿผ",oil="๐Ÿ›ข",diamond_shape_with_a_dot_inside="๐Ÿ’ ",barber="๐Ÿ’ˆ",metal_tone3="๐Ÿค˜๐Ÿฝ",ice_cream="๐Ÿจ",rowboat_tone4="๐Ÿšฃ๐Ÿพ",burrito="๐ŸŒฏ",metal_tone1="๐Ÿค˜๐Ÿป",joystick="๐Ÿ•น",rowboat_tone1="๐Ÿšฃ๐Ÿป",taxi="๐Ÿš•",u7533="๐Ÿˆธ",racehorse="๐ŸŽ",snowboarder="๐Ÿ‚",thinking="๐Ÿค”",wave_tone1="๐Ÿ‘‹๐Ÿป",wave_tone2="๐Ÿ‘‹๐Ÿผ",wave_tone3="๐Ÿ‘‹๐Ÿฝ",wave_tone4="๐Ÿ‘‹๐Ÿพ",wave_tone5="๐Ÿ‘‹๐Ÿฟ",desktop="๐Ÿ–ฅ",stopwatch="โฑ",pill="๐Ÿ’Š",skier="โ›ท",orange_book="๐Ÿ“™",dart="๐ŸŽฏ",disappointed="๐Ÿ˜ž",grin="๐Ÿ˜",place_of_worship="๐Ÿ›",japanese_goblin="๐Ÿ‘บ",arrows_counterclockwise="๐Ÿ”„",laughing="๐Ÿ˜†",clap="๐Ÿ‘",left_right_arrow="โ†”",japanese_castle="๐Ÿฏ",nail_care_tone4="๐Ÿ’…๐Ÿพ",nail_care_tone5="๐Ÿ’…๐Ÿฟ",nail_care_tone2="๐Ÿ’…๐Ÿผ",nail_care_tone3="๐Ÿ’…๐Ÿฝ",nail_care_tone1="๐Ÿ’…๐Ÿป",raised_hand_tone4="โœ‹๐Ÿพ",raised_hand_tone5="โœ‹๐Ÿฟ",raised_hand_tone1="โœ‹๐Ÿป",raised_hand_tone2="โœ‹๐Ÿผ",raised_hand_tone3="โœ‹๐Ÿฝ",point_left_tone3="๐Ÿ‘ˆ๐Ÿฝ",point_left_tone2="๐Ÿ‘ˆ๐Ÿผ",tanabata_tree="๐ŸŽ‹",point_left_tone5="๐Ÿ‘ˆ๐Ÿฟ",point_left_tone4="๐Ÿ‘ˆ๐Ÿพ",o2="๐Ÿ…พ",knife="๐Ÿ”ช",volcano="๐ŸŒ‹",kissing_heart="๐Ÿ˜˜",on="๐Ÿ”›",ok="๐Ÿ†—",package="๐Ÿ“ฆ",island="๐Ÿ",arrow_right="โžก",chart_with_downwards_trend="๐Ÿ“‰",haircut_tone3="๐Ÿ’‡๐Ÿฝ",wolf="๐Ÿบ",ox="๐Ÿ‚",dagger="๐Ÿ—ก",full_moon_with_face="๐ŸŒ",syringe="๐Ÿ’‰",flag_by="๐Ÿ‡ง๐Ÿ‡พ",flag_bz="๐Ÿ‡ง๐Ÿ‡ฟ",flag_bq="๐Ÿ‡ง๐Ÿ‡ถ",flag_br="๐Ÿ‡ง๐Ÿ‡ท",flag_bs="๐Ÿ‡ง๐Ÿ‡ธ",flag_bt="๐Ÿ‡ง๐Ÿ‡น",flag_bv="๐Ÿ‡ง๐Ÿ‡ป",flag_bw="๐Ÿ‡ง๐Ÿ‡ผ",flag_bh="๐Ÿ‡ง๐Ÿ‡ญ",flag_bi="๐Ÿ‡ง๐Ÿ‡ฎ",flag_bj="๐Ÿ‡ง๐Ÿ‡ฏ",flag_bl="๐Ÿ‡ง๐Ÿ‡ฑ",flag_bm="๐Ÿ‡ง๐Ÿ‡ฒ",flag_bn="๐Ÿ‡ง๐Ÿ‡ณ",flag_bo="๐Ÿ‡ง๐Ÿ‡ด",flag_ba="๐Ÿ‡ง๐Ÿ‡ฆ",flag_bb="๐Ÿ‡ง๐Ÿ‡ง",flag_bd="๐Ÿ‡ง๐Ÿ‡ฉ",flag_be="๐Ÿ‡ง๐Ÿ‡ช",flag_bf="๐Ÿ‡ง๐Ÿ‡ซ",flag_bg="๐Ÿ‡ง๐Ÿ‡ฌ",satellite_orbital="๐Ÿ›ฐ",radio_button="๐Ÿ”˜",arrow_heading_down="โคต",rage="๐Ÿ˜ก",whale2="๐Ÿ‹",vhs="๐Ÿ“ผ",hand_splayed_tone3="๐Ÿ–๐Ÿฝ",strawberry="๐Ÿ“",["non-potable_water"]="๐Ÿšฑ",hand_splayed_tone5="๐Ÿ–๐Ÿฟ",star2="๐ŸŒŸ",toilet="๐Ÿšฝ",ab="๐Ÿ†Ž",cinema="๐ŸŽฆ",floppy_disk="๐Ÿ’พ",princess_tone4="๐Ÿ‘ธ๐Ÿพ",princess_tone5="๐Ÿ‘ธ๐Ÿฟ",princess_tone2="๐Ÿ‘ธ๐Ÿผ",nerd="๐Ÿค“",telephone_receiver="๐Ÿ“ž",princess_tone1="๐Ÿ‘ธ๐Ÿป",arrow_double_down="โฌ",clock1030="๐Ÿ•ฅ",flag_pr="๐Ÿ‡ต๐Ÿ‡ท",flag_ps="๐Ÿ‡ต๐Ÿ‡ธ",poop="๐Ÿ’ฉ",flag_pw="๐Ÿ‡ต๐Ÿ‡ผ",flag_pt="๐Ÿ‡ต๐Ÿ‡น",flag_py="๐Ÿ‡ต๐Ÿ‡พ",pear="๐Ÿ",m="โ“‚",flag_pa="๐Ÿ‡ต๐Ÿ‡ฆ",flag_pf="๐Ÿ‡ต๐Ÿ‡ซ",flag_pg="๐Ÿ‡ต๐Ÿ‡ฌ",flag_pe="๐Ÿ‡ต๐Ÿ‡ช",flag_pk="๐Ÿ‡ต๐Ÿ‡ฐ",flag_ph="๐Ÿ‡ต๐Ÿ‡ญ",flag_pn="๐Ÿ‡ต๐Ÿ‡ณ",flag_pl="๐Ÿ‡ต๐Ÿ‡ฑ",flag_pm="๐Ÿ‡ต๐Ÿ‡ฒ",mask="๐Ÿ˜ท",hushed="๐Ÿ˜ฏ",sunrise_over_mountains="๐ŸŒ„",partly_sunny="โ›…",dollar="๐Ÿ’ต",helmet_with_cross="โ›‘",smoking="๐Ÿšฌ",no_bicycles="๐Ÿšณ",man_with_gua_pi_mao="๐Ÿ‘ฒ",tv="๐Ÿ“บ",open_hands="๐Ÿ‘",rotating_light="๐Ÿšจ",information_desk_person_tone4="๐Ÿ’๐Ÿพ",information_desk_person_tone5="๐Ÿ’๐Ÿฟ",part_alternation_mark="ใ€ฝ",pray_tone5="๐Ÿ™๐Ÿฟ",pray_tone4="๐Ÿ™๐Ÿพ",pray_tone3="๐Ÿ™๐Ÿฝ",pray_tone2="๐Ÿ™๐Ÿผ",pray_tone1="๐Ÿ™๐Ÿป",smile="๐Ÿ˜„",large_blue_circle="๐Ÿ”ต",man_tone4="๐Ÿ‘จ๐Ÿพ",man_tone5="๐Ÿ‘จ๐Ÿฟ",fax="๐Ÿ“ ",woman="๐Ÿ‘ฉ",man_tone1="๐Ÿ‘จ๐Ÿป",man_tone2="๐Ÿ‘จ๐Ÿผ",man_tone3="๐Ÿ‘จ๐Ÿฝ",eye_in_speech_bubble="๐Ÿ‘๐Ÿ—จ",blowfish="๐Ÿก",card_box="๐Ÿ—ƒ",ticket="๐ŸŽซ",ramen="๐Ÿœ",twisted_rightwards_arrows="๐Ÿ”€",swimmer_tone4="๐ŸŠ๐Ÿพ",swimmer_tone5="๐ŸŠ๐Ÿฟ",swimmer_tone1="๐ŸŠ๐Ÿป",swimmer_tone2="๐ŸŠ๐Ÿผ",swimmer_tone3="๐ŸŠ๐Ÿฝ",saxophone="๐ŸŽท",bath_tone1="๐Ÿ›€๐Ÿป",notebook_with_decorative_cover="๐Ÿ“”",bath_tone3="๐Ÿ›€๐Ÿฝ",ten="๐Ÿ”Ÿ",raising_hand_tone4="๐Ÿ™‹๐Ÿพ",tea="๐Ÿต",raising_hand_tone1="๐Ÿ™‹๐Ÿป",raising_hand_tone2="๐Ÿ™‹๐Ÿผ",raising_hand_tone3="๐Ÿ™‹๐Ÿฝ",zero="0โƒฃ",ribbon="๐ŸŽ€",santa_tone1="๐ŸŽ…๐Ÿป",abc="๐Ÿ”ค",clock="๐Ÿ•ฐ",purple_heart="๐Ÿ’œ",bow_tone1="๐Ÿ™‡๐Ÿป",no_smoking="๐Ÿšญ",flag_cl="๐Ÿ‡จ๐Ÿ‡ฑ",surfer="๐Ÿ„",newspaper="๐Ÿ“ฐ",busstop="๐Ÿš",new_moon="๐ŸŒ‘",traffic_light="๐Ÿšฅ",thumbsup="๐Ÿ‘",no_entry="โ›”",name_badge="๐Ÿ“›",classical_building="๐Ÿ›",hamster="๐Ÿน",pick="โ›",two_women_holding_hands="๐Ÿ‘ญ",family_mmbb="๐Ÿ‘จ๐Ÿ‘จ๐Ÿ‘ฆ๐Ÿ‘ฆ",family="๐Ÿ‘ช",rice_cracker="๐Ÿ˜",wind_blowing_face="๐ŸŒฌ",inbox_tray="๐Ÿ“ฅ",tired_face="๐Ÿ˜ซ",carousel_horse="๐ŸŽ ",eye="๐Ÿ‘",poodle="๐Ÿฉ",chestnut="๐ŸŒฐ",slight_smile="๐Ÿ™‚",mailbox_closed="๐Ÿ“ช",cloud_tornado="๐ŸŒช",jack_o_lantern="๐ŸŽƒ",lifter_tone3="๐Ÿ‹๐Ÿฝ",lifter_tone2="๐Ÿ‹๐Ÿผ",lifter_tone1="๐Ÿ‹๐Ÿป",lifter_tone5="๐Ÿ‹๐Ÿฟ",lifter_tone4="๐Ÿ‹๐Ÿพ",nine="9โƒฃ",chocolate_bar="๐Ÿซ",v="โœŒ",man_with_turban_tone4="๐Ÿ‘ณ๐Ÿพ",man_with_turban_tone5="๐Ÿ‘ณ๐Ÿฟ",man_with_turban_tone2="๐Ÿ‘ณ๐Ÿผ",man_with_turban_tone3="๐Ÿ‘ณ๐Ÿฝ",man_with_turban_tone1="๐Ÿ‘ณ๐Ÿป",family_wwbb="๐Ÿ‘ฉ๐Ÿ‘ฉ๐Ÿ‘ฆ๐Ÿ‘ฆ",hamburger="๐Ÿ”",accept="๐Ÿ‰‘",airplane="โœˆ",dress="๐Ÿ‘—",speedboat="๐Ÿšค",ledger="๐Ÿ“’",goat="๐Ÿ",flag_ae="๐Ÿ‡ฆ๐Ÿ‡ช",flag_ad="๐Ÿ‡ฆ๐Ÿ‡ฉ",flag_ag="๐Ÿ‡ฆ๐Ÿ‡ฌ",flag_af="๐Ÿ‡ฆ๐Ÿ‡ซ",flag_ac="๐Ÿ‡ฆ๐Ÿ‡จ",flag_am="๐Ÿ‡ฆ๐Ÿ‡ฒ",flag_al="๐Ÿ‡ฆ๐Ÿ‡ฑ",flag_ao="๐Ÿ‡ฆ๐Ÿ‡ด",flag_ai="๐Ÿ‡ฆ๐Ÿ‡ฎ",flag_au="๐Ÿ‡ฆ๐Ÿ‡บ",flag_at="๐Ÿ‡ฆ๐Ÿ‡น",fork_and_knife="๐Ÿด",fast_forward="โฉ",flag_as="๐Ÿ‡ฆ๐Ÿ‡ธ",flag_ar="๐Ÿ‡ฆ๐Ÿ‡ท",cow2="๐Ÿ„",flag_ax="๐Ÿ‡ฆ๐Ÿ‡ฝ",flag_az="๐Ÿ‡ฆ๐Ÿ‡ฟ",a="๐Ÿ…ฐ",volleyball="๐Ÿ",dragon="๐Ÿ‰",wrench="๐Ÿ”ง",point_up_2="๐Ÿ‘†",egg="๐Ÿณ",small_red_triangle="๐Ÿ”บ",soon="๐Ÿ”œ",bow_tone4="๐Ÿ™‡๐Ÿพ",joy_cat="๐Ÿ˜น",pray="๐Ÿ™",dark_sunglasses="๐Ÿ•ถ",rugby_football="๐Ÿ‰",soccer="โšฝ",dolls="๐ŸŽŽ",monkey_face="๐Ÿต",clap_tone3="๐Ÿ‘๐Ÿฝ",bar_chart="๐Ÿ“Š",european_castle="๐Ÿฐ",military_medal="๐ŸŽ–",frame_photo="๐Ÿ–ผ",rice_ball="๐Ÿ™",trolleybus="๐ŸšŽ",older_woman="๐Ÿ‘ต",information_source="โ„น",postal_horn="๐Ÿ“ฏ",house="๐Ÿ ",fish="๐ŸŸ",bride_with_veil="๐Ÿ‘ฐ",fist="โœŠ",lipstick="๐Ÿ’„",fountain="โ›ฒ",cyclone="๐ŸŒ€",thumbsdown_tone2="๐Ÿ‘Ž๐Ÿผ",thumbsdown_tone3="๐Ÿ‘Ž๐Ÿฝ",thumbsdown_tone1="๐Ÿ‘Ž๐Ÿป",thumbsdown_tone4="๐Ÿ‘Ž๐Ÿพ",thumbsdown_tone5="๐Ÿ‘Ž๐Ÿฟ",cookie="๐Ÿช",heartbeat="๐Ÿ’“",blush="๐Ÿ˜Š",fire_engine="๐Ÿš’",feet="๐Ÿพ",horse="๐Ÿด",blossom="๐ŸŒผ",crossed_swords="โš”",station="๐Ÿš‰",clock730="๐Ÿ•ข",banana="๐ŸŒ",relieved="๐Ÿ˜Œ",hotel="๐Ÿจ",park="๐Ÿž",aerial_tramway="๐Ÿšก",flag_sd="๐Ÿ‡ธ๐Ÿ‡ฉ",panda_face="๐Ÿผ",b="๐Ÿ…ฑ",flag_sx="๐Ÿ‡ธ๐Ÿ‡ฝ",six_pointed_star="๐Ÿ”ฏ",shaved_ice="๐Ÿง",chipmunk="๐Ÿฟ",mountain="โ›ฐ",koala="๐Ÿจ",white_small_square="โ–ซ",open_hands_tone2="๐Ÿ‘๐Ÿผ",open_hands_tone3="๐Ÿ‘๐Ÿฝ",u55b6="๐Ÿˆบ",open_hands_tone1="๐Ÿ‘๐Ÿป",open_hands_tone4="๐Ÿ‘๐Ÿพ",open_hands_tone5="๐Ÿ‘๐Ÿฟ",baby_tone5="๐Ÿ‘ถ๐Ÿฟ",baby_tone4="๐Ÿ‘ถ๐Ÿพ",baby_tone3="๐Ÿ‘ถ๐Ÿฝ",baby_tone2="๐Ÿ‘ถ๐Ÿผ",baby_tone1="๐Ÿ‘ถ๐Ÿป",chart="๐Ÿ’น",beach_umbrella="โ›ฑ",basketball_player_tone5="โ›น๐Ÿฟ",basketball_player_tone4="โ›น๐Ÿพ",basketball_player_tone1="โ›น๐Ÿป",basketball_player_tone3="โ›น๐Ÿฝ",basketball_player_tone2="โ›น๐Ÿผ",mans_shoe="๐Ÿ‘ž",shinto_shrine="โ›ฉ",ideograph_advantage="๐Ÿ‰",airplane_arriving="๐Ÿ›ฌ",golf="โ›ณ",minidisc="๐Ÿ’ฝ",hugging="๐Ÿค—",crayon="๐Ÿ–",point_down="๐Ÿ‘‡",copyright="ยฉ",person_with_pouting_face_tone2="๐Ÿ™Ž๐Ÿผ",person_with_pouting_face_tone3="๐Ÿ™Ž๐Ÿฝ",person_with_pouting_face_tone1="๐Ÿ™Ž๐Ÿป",person_with_pouting_face_tone4="๐Ÿ™Ž๐Ÿพ",person_with_pouting_face_tone5="๐Ÿ™Ž๐Ÿฟ",busts_in_silhouette="๐Ÿ‘ฅ",alarm_clock="โฐ",couplekiss="๐Ÿ’",circus_tent="๐ŸŽช",sunny="โ˜€",incoming_envelope="๐Ÿ“จ",yellow_heart="๐Ÿ’›",cry="๐Ÿ˜ข",x="โŒ",arrow_up_small="๐Ÿ”ผ",art="๐ŸŽจ",surfer_tone1="๐Ÿ„๐Ÿป",bride_with_veil_tone4="๐Ÿ‘ฐ๐Ÿพ",bride_with_veil_tone5="๐Ÿ‘ฐ๐Ÿฟ",bride_with_veil_tone2="๐Ÿ‘ฐ๐Ÿผ",bride_with_veil_tone3="๐Ÿ‘ฐ๐Ÿฝ",bride_with_veil_tone1="๐Ÿ‘ฐ๐Ÿป",hibiscus="๐ŸŒบ",black_joker="๐Ÿƒ",raised_hand="โœ‹",no_mouth="๐Ÿ˜ถ",basketball_player="โ›น",champagne="๐Ÿพ",no_entry_sign="๐Ÿšซ",older_man="๐Ÿ‘ด",moyai="๐Ÿ—ฟ",mailbox="๐Ÿ“ซ",slight_frown="๐Ÿ™",statue_of_liberty="๐Ÿ—ฝ",mega="๐Ÿ“ฃ",eggplant="๐Ÿ†",rose="๐ŸŒน",bell="๐Ÿ””",battery="๐Ÿ”‹",wastebasket="๐Ÿ—‘",dancer="๐Ÿ’ƒ",page_facing_up="๐Ÿ“„",church="โ›ช",underage="๐Ÿ”ž",secret="ใŠ™",clock430="๐Ÿ•Ÿ",fork_knife_plate="๐Ÿฝ",u7981="๐Ÿˆฒ",fire="๐Ÿ”ฅ",cold_sweat="๐Ÿ˜ฐ",flag_er="๐Ÿ‡ช๐Ÿ‡ท",family_mwgg="๐Ÿ‘จ๐Ÿ‘ฉ๐Ÿ‘ง๐Ÿ‘ง",heart_eyes="๐Ÿ˜",guardsman_tone1="๐Ÿ’‚๐Ÿป",guardsman_tone2="๐Ÿ’‚๐Ÿผ",guardsman_tone3="๐Ÿ’‚๐Ÿฝ",guardsman_tone4="๐Ÿ’‚๐Ÿพ",earth_africa="๐ŸŒ",arrow_right_hook="โ†ช",spy_tone2="๐Ÿ•ต๐Ÿผ",closed_umbrella="๐ŸŒ‚",bikini="๐Ÿ‘™",vertical_traffic_light="๐Ÿšฆ",kissing="๐Ÿ˜—",loop="โžฟ",potable_water="๐Ÿšฐ",pound="๐Ÿ’ท",["fleur-de-lis"]="โšœ",key2="๐Ÿ—",heavy_dollar_sign="๐Ÿ’ฒ",shamrock="โ˜˜",boy_tone4="๐Ÿ‘ฆ๐Ÿพ",shirt="๐Ÿ‘•",kimono="๐Ÿ‘˜",left_luggage="๐Ÿ›…",meat_on_bone="๐Ÿ–",ok_woman_tone4="๐Ÿ™†๐Ÿพ",ok_woman_tone5="๐Ÿ™†๐Ÿฟ",arrow_heading_up="โคด",calendar_spiral="๐Ÿ—“",snail="๐ŸŒ",ok_woman_tone1="๐Ÿ™†๐Ÿป",arrow_down_small="๐Ÿ”ฝ",leopard="๐Ÿ†",paperclips="๐Ÿ–‡",cityscape="๐Ÿ™",woman_tone1="๐Ÿ‘ฉ๐Ÿป",slot_machine="๐ŸŽฐ",woman_tone3="๐Ÿ‘ฉ๐Ÿฝ",woman_tone4="๐Ÿ‘ฉ๐Ÿพ",woman_tone5="๐Ÿ‘ฉ๐Ÿฟ",euro="๐Ÿ’ถ",musical_score="๐ŸŽผ",triangular_ruler="๐Ÿ“",flags="๐ŸŽ",five="5โƒฃ",love_hotel="๐Ÿฉ",hotdog="๐ŸŒญ",speak_no_evil="๐Ÿ™Š",eyeglasses="๐Ÿ‘“",dancer_tone4="๐Ÿ’ƒ๐Ÿพ",dancer_tone5="๐Ÿ’ƒ๐Ÿฟ",vulcan_tone4="๐Ÿ––๐Ÿพ",bridge_at_night="๐ŸŒ‰",writing_hand_tone1="โœ๐Ÿป",couch="๐Ÿ›‹",vulcan_tone1="๐Ÿ––๐Ÿป",vulcan_tone2="๐Ÿ––๐Ÿผ",vulcan_tone3="๐Ÿ––๐Ÿฝ",womans_hat="๐Ÿ‘’",sandal="๐Ÿ‘ก",cherries="๐Ÿ’",full_moon="๐ŸŒ•",flag_om="๐Ÿ‡ด๐Ÿ‡ฒ",play_pause="โฏ",couple="๐Ÿ‘ซ",money_mouth="๐Ÿค‘",womans_clothes="๐Ÿ‘š",globe_with_meridians="๐ŸŒ",bath_tone5="๐Ÿ›€๐Ÿฟ",bangbang="โ€ผ",stuck_out_tongue_winking_eye="๐Ÿ˜œ",heart="โค",bamboo="๐ŸŽ",mahjong="๐Ÿ€„",waning_gibbous_moon="๐ŸŒ–",back="๐Ÿ”™",point_up_2_tone4="๐Ÿ‘†๐Ÿพ",point_up_2_tone5="๐Ÿ‘†๐Ÿฟ",lips="๐Ÿ‘„",point_up_2_tone1="๐Ÿ‘†๐Ÿป",point_up_2_tone2="๐Ÿ‘†๐Ÿผ",point_up_2_tone3="๐Ÿ‘†๐Ÿฝ",candle="๐Ÿ•ฏ",middle_finger_tone3="๐Ÿ–•๐Ÿฝ",middle_finger_tone2="๐Ÿ–•๐Ÿผ",middle_finger_tone1="๐Ÿ–•๐Ÿป",middle_finger_tone5="๐Ÿ–•๐Ÿฟ",middle_finger_tone4="๐Ÿ–•๐Ÿพ",heavy_minus_sign="โž–",nose="๐Ÿ‘ƒ",zzz="๐Ÿ’ค",stew="๐Ÿฒ",santa="๐ŸŽ…",tropical_fish="๐Ÿ ",point_up_tone1="โ˜๐Ÿป",point_up_tone3="โ˜๐Ÿฝ",point_up_tone2="โ˜๐Ÿผ",point_up_tone5="โ˜๐Ÿฟ",point_up_tone4="โ˜๐Ÿพ",field_hockey="๐Ÿ‘",school_satchel="๐ŸŽ’",womens="๐Ÿšบ",baby_symbol="๐Ÿšผ",baby_chick="๐Ÿค",ok_hand_tone2="๐Ÿ‘Œ๐Ÿผ",ok_hand_tone3="๐Ÿ‘Œ๐Ÿฝ",ok_hand_tone1="๐Ÿ‘Œ๐Ÿป",ok_hand_tone4="๐Ÿ‘Œ๐Ÿพ",ok_hand_tone5="๐Ÿ‘Œ๐Ÿฟ",family_mmgb="๐Ÿ‘จ๐Ÿ‘จ๐Ÿ‘ง๐Ÿ‘ฆ",last_quarter_moon="๐ŸŒ—",tada="๐ŸŽ‰",clock530="๐Ÿ• ",question="โ“",registered="ยฎ",level_slider="๐ŸŽš",black_circle="โšซ",atom="โš›",penguin="๐Ÿง",electric_plug="๐Ÿ”Œ",skull="๐Ÿ’€",kiss_mm="๐Ÿ‘จโค๐Ÿ’‹๐Ÿ‘จ",walking_tone4="๐Ÿšถ๐Ÿพ",fries="๐ŸŸ",up="๐Ÿ†™",walking_tone3="๐Ÿšถ๐Ÿฝ",walking_tone2="๐Ÿšถ๐Ÿผ",athletic_shoe="๐Ÿ‘Ÿ",hatched_chick="๐Ÿฅ",black_nib="โœ’",black_large_square="โฌ›",bow_and_arrow="๐Ÿน",rainbow="๐ŸŒˆ",metal_tone5="๐Ÿค˜๐Ÿฟ",metal_tone4="๐Ÿค˜๐Ÿพ",lemon="๐Ÿ‹",metal_tone2="๐Ÿค˜๐Ÿผ",peach="๐Ÿ‘",peace="โ˜ฎ",steam_locomotive="๐Ÿš‚",oncoming_bus="๐Ÿš",heart_eyes_cat="๐Ÿ˜ป",smiley="๐Ÿ˜ƒ",haircut_tone1="๐Ÿ’‡๐Ÿป",haircut_tone2="๐Ÿ’‡๐Ÿผ",u6e80="๐Ÿˆต",haircut_tone4="๐Ÿ’‡๐Ÿพ",haircut_tone5="๐Ÿ’‡๐Ÿฟ",black_medium_square="โ—ผ",closed_book="๐Ÿ“•",desert="๐Ÿœ",expressionless="๐Ÿ˜‘",dvd="๐Ÿ“€",construction_worker_tone2="๐Ÿ‘ท๐Ÿผ",construction_worker_tone3="๐Ÿ‘ท๐Ÿฝ",construction_worker_tone4="๐Ÿ‘ท๐Ÿพ",construction_worker_tone5="๐Ÿ‘ท๐Ÿฟ",mag_right="๐Ÿ”Ž",bento="๐Ÿฑ",scroll="๐Ÿ“œ",flag_nl="๐Ÿ‡ณ๐Ÿ‡ฑ",flag_no="๐Ÿ‡ณ๐Ÿ‡ด",flag_ni="๐Ÿ‡ณ๐Ÿ‡ฎ",european_post_office="๐Ÿค",flag_ne="๐Ÿ‡ณ๐Ÿ‡ช",flag_nf="๐Ÿ‡ณ๐Ÿ‡ซ",flag_ng="๐Ÿ‡ณ๐Ÿ‡ฌ",flag_na="๐Ÿ‡ณ๐Ÿ‡ฆ",flag_nc="๐Ÿ‡ณ๐Ÿ‡จ",alien="๐Ÿ‘ฝ",first_quarter_moon_with_face="๐ŸŒ›",flag_nz="๐Ÿ‡ณ๐Ÿ‡ฟ",flag_nu="๐Ÿ‡ณ๐Ÿ‡บ",golfer="๐ŸŒ",flag_np="๐Ÿ‡ณ๐Ÿ‡ต",flag_nr="๐Ÿ‡ณ๐Ÿ‡ท",anguished="๐Ÿ˜ง",mosque="๐Ÿ•Œ",point_left_tone1="๐Ÿ‘ˆ๐Ÿป",ear_tone1="๐Ÿ‘‚๐Ÿป",ear_tone2="๐Ÿ‘‚๐Ÿผ",ear_tone3="๐Ÿ‘‚๐Ÿฝ",ear_tone4="๐Ÿ‘‚๐Ÿพ",ear_tone5="๐Ÿ‘‚๐Ÿฟ",eight_pointed_black_star="โœด",wave="๐Ÿ‘‹",runner_tone5="๐Ÿƒ๐Ÿฟ",runner_tone4="๐Ÿƒ๐Ÿพ",runner_tone3="๐Ÿƒ๐Ÿฝ",runner_tone2="๐Ÿƒ๐Ÿผ",runner_tone1="๐Ÿƒ๐Ÿป",railway_car="๐Ÿšƒ",notes="๐ŸŽถ",no_good="๐Ÿ™…",trackball="๐Ÿ–ฒ",spaghetti="๐Ÿ",love_letter="๐Ÿ’Œ",clipboard="๐Ÿ“‹",baby_bottle="๐Ÿผ",bird="๐Ÿฆ",card_index="๐Ÿ“‡",punch="๐Ÿ‘Š",leo="โ™Œ",house_with_garden="๐Ÿก",family_wwgg="๐Ÿ‘ฉ๐Ÿ‘ฉ๐Ÿ‘ง๐Ÿ‘ง",family_wwgb="๐Ÿ‘ฉ๐Ÿ‘ฉ๐Ÿ‘ง๐Ÿ‘ฆ",see_no_evil="๐Ÿ™ˆ",metro="๐Ÿš‡",popcorn="๐Ÿฟ",apple="๐ŸŽ",scales="โš–",sleeping_accommodation="๐Ÿ›Œ",clock230="๐Ÿ•",tools="๐Ÿ› ",cloud="โ˜",honey_pot="๐Ÿฏ",ballot_box="๐Ÿ—ณ",frog="๐Ÿธ",camera="๐Ÿ“ท",crab="๐Ÿฆ€",video_camera="๐Ÿ“น",pencil="๐Ÿ“",thunder_cloud_rain="โ›ˆ",mountain_bicyclist="๐Ÿšต",tangerine="๐ŸŠ",train="๐Ÿš‹",rabbit="๐Ÿฐ",baby="๐Ÿ‘ถ",palm_tree="๐ŸŒด",capital_abcd="๐Ÿ” ",put_litter_in_its_place="๐Ÿšฎ",coffin="โšฐ",abcd="๐Ÿ”ก",lock="๐Ÿ”’",pig2="๐Ÿ–",family_mwg="๐Ÿ‘จ๐Ÿ‘ฉ๐Ÿ‘ง",point_right_tone5="๐Ÿ‘‰๐Ÿฟ",trumpet="๐ŸŽบ",film_frames="๐ŸŽž",six="6โƒฃ",leftwards_arrow_with_hook="โ†ฉ",earth_asia="๐ŸŒ",heavy_check_mark="โœ”",notebook="๐Ÿ““",taco="๐ŸŒฎ",tomato="๐Ÿ…",robot="๐Ÿค–",mute="๐Ÿ”‡",symbols="๐Ÿ”ฃ",motorcycle="๐Ÿ",thermometer_face="๐Ÿค’",paperclip="๐Ÿ“Ž",moneybag="๐Ÿ’ฐ",neutral_face="๐Ÿ˜",white_sun_rain_cloud="๐ŸŒฆ",snake="๐Ÿ",kiss="๐Ÿ’‹",blue_car="๐Ÿš™",confetti_ball="๐ŸŽŠ",tram="๐ŸšŠ",repeat_one="๐Ÿ”‚",smiley_cat="๐Ÿ˜บ",beginner="๐Ÿ”ฐ",mobile_phone_off="๐Ÿ“ด",books="๐Ÿ“š",["8ball"]="๐ŸŽฑ",cocktail="๐Ÿธ",flag_ge="๐Ÿ‡ฌ๐Ÿ‡ช",horse_racing_tone2="๐Ÿ‡๐Ÿผ",flag_mh="๐Ÿ‡ฒ๐Ÿ‡ญ",flag_mk="๐Ÿ‡ฒ๐Ÿ‡ฐ",flag_mm="๐Ÿ‡ฒ๐Ÿ‡ฒ",flag_ml="๐Ÿ‡ฒ๐Ÿ‡ฑ",flag_mo="๐Ÿ‡ฒ๐Ÿ‡ด",flag_mn="๐Ÿ‡ฒ๐Ÿ‡ณ",flag_ma="๐Ÿ‡ฒ๐Ÿ‡ฆ",flag_mc="๐Ÿ‡ฒ๐Ÿ‡จ",flag_me="๐Ÿ‡ฒ๐Ÿ‡ช",flag_md="๐Ÿ‡ฒ๐Ÿ‡ฉ",flag_mg="๐Ÿ‡ฒ๐Ÿ‡ฌ",flag_mf="๐Ÿ‡ฒ๐Ÿ‡ซ",flag_my="๐Ÿ‡ฒ๐Ÿ‡พ",flag_mx="๐Ÿ‡ฒ๐Ÿ‡ฝ",flag_mz="๐Ÿ‡ฒ๐Ÿ‡ฟ",mountain_snow="๐Ÿ”",flag_mp="๐Ÿ‡ฒ๐Ÿ‡ต",flag_ms="๐Ÿ‡ฒ๐Ÿ‡ธ",flag_mr="๐Ÿ‡ฒ๐Ÿ‡ท",flag_mu="๐Ÿ‡ฒ๐Ÿ‡บ",flag_mt="๐Ÿ‡ฒ๐Ÿ‡น",flag_mw="๐Ÿ‡ฒ๐Ÿ‡ผ",flag_mv="๐Ÿ‡ฒ๐Ÿ‡ป",timer="โฒ",passport_control="๐Ÿ›‚",small_blue_diamond="๐Ÿ”น",lion_face="๐Ÿฆ",white_check_mark="โœ…",bouquet="๐Ÿ’",track_previous="โฎ",monkey="๐Ÿ’",tone4="๐Ÿพ",closed_lock_with_key="๐Ÿ”",family_wwb="๐Ÿ‘ฉ๐Ÿ‘ฉ๐Ÿ‘ฆ",family_wwg="๐Ÿ‘ฉ๐Ÿ‘ฉ๐Ÿ‘ง",tone1="๐Ÿป",crescent_moon="๐ŸŒ™",shell="๐Ÿš",gear="โš™",tone2="๐Ÿผ",small_red_triangle_down="๐Ÿ”ป",nut_and_bolt="๐Ÿ”ฉ",umbrella2="โ˜‚",unamused="๐Ÿ˜’",fuelpump="โ›ฝ",bed="๐Ÿ›",bee="๐Ÿ",round_pushpin="๐Ÿ“",flag_white="๐Ÿณ",microphone="๐ŸŽค",bus="๐ŸšŒ",eight="8โƒฃ",handbag="๐Ÿ‘œ",medal="๐Ÿ…",arrows_clockwise="๐Ÿ”ƒ",urn="โšฑ",bookmark_tabs="๐Ÿ“‘",new_moon_with_face="๐ŸŒš",fallen_leaf="๐Ÿ‚",horse_racing="๐Ÿ‡",chicken="๐Ÿ”",ear="๐Ÿ‘‚",wheel_of_dharma="โ˜ธ",arrow_lower_right="โ†˜",man_with_gua_pi_mao_tone5="๐Ÿ‘ฒ๐Ÿฟ",scorpion="๐Ÿฆ‚",waning_crescent_moon="๐ŸŒ˜",man_with_gua_pi_mao_tone2="๐Ÿ‘ฒ๐Ÿผ",man_with_gua_pi_mao_tone1="๐Ÿ‘ฒ๐Ÿป",bug="๐Ÿ›",virgo="โ™",libra="โ™Ž",angel_tone1="๐Ÿ‘ผ๐Ÿป",angel_tone3="๐Ÿ‘ผ๐Ÿฝ",angel_tone2="๐Ÿ‘ผ๐Ÿผ",angel_tone5="๐Ÿ‘ผ๐Ÿฟ",sagittarius="โ™",bear="๐Ÿป",information_desk_person_tone3="๐Ÿ’๐Ÿฝ",no_mobile_phones="๐Ÿ“ต",hand_splayed="๐Ÿ–",motorboat="๐Ÿ›ฅ",calling="๐Ÿ“ฒ",interrobang="โ‰",oncoming_taxi="๐Ÿš–",flag_lt="๐Ÿ‡ฑ๐Ÿ‡น",flag_lu="๐Ÿ‡ฑ๐Ÿ‡บ",flag_lr="๐Ÿ‡ฑ๐Ÿ‡ท",flag_ls="๐Ÿ‡ฑ๐Ÿ‡ธ",flag_ly="๐Ÿ‡ฑ๐Ÿ‡พ",bellhop="๐Ÿ›Ž",arrow_down="โฌ‡",flag_lc="๐Ÿ‡ฑ๐Ÿ‡จ",flag_la="๐Ÿ‡ฑ๐Ÿ‡ฆ",flag_lk="๐Ÿ‡ฑ๐Ÿ‡ฐ",flag_li="๐Ÿ‡ฑ๐Ÿ‡ฎ",ferris_wheel="๐ŸŽก",hand_splayed_tone2="๐Ÿ–๐Ÿผ",large_blue_diamond="๐Ÿ”ท",cat2="๐Ÿˆ",icecream="๐Ÿฆ",tent="โ›บ",joy="๐Ÿ˜‚",hand_splayed_tone4="๐Ÿ–๐Ÿพ",file_cabinet="๐Ÿ—„",key="๐Ÿ”‘",weary="๐Ÿ˜ฉ",bath_tone2="๐Ÿ›€๐Ÿผ",flag_lv="๐Ÿ‡ฑ๐Ÿ‡ป",low_brightness="๐Ÿ”…",rowboat_tone5="๐Ÿšฃ๐Ÿฟ",rowboat_tone2="๐Ÿšฃ๐Ÿผ",rowboat_tone3="๐Ÿšฃ๐Ÿฝ",four_leaf_clover="๐Ÿ€",space_invader="๐Ÿ‘พ",cl="๐Ÿ†‘",cd="๐Ÿ’ฟ",bath_tone4="๐Ÿ›€๐Ÿพ",flag_za="๐Ÿ‡ฟ๐Ÿ‡ฆ",swimmer="๐ŸŠ",wavy_dash="ใ€ฐ",flag_zm="๐Ÿ‡ฟ๐Ÿ‡ฒ",flag_zw="๐Ÿ‡ฟ๐Ÿ‡ผ",raised_hands_tone5="๐Ÿ™Œ๐Ÿฟ",two_hearts="๐Ÿ’•",bulb="๐Ÿ’ก",cop_tone4="๐Ÿ‘ฎ๐Ÿพ",cop_tone5="๐Ÿ‘ฎ๐Ÿฟ",cop_tone2="๐Ÿ‘ฎ๐Ÿผ",cop_tone3="๐Ÿ‘ฎ๐Ÿฝ",cop_tone1="๐Ÿ‘ฎ๐Ÿป",open_file_folder="๐Ÿ“‚",homes="๐Ÿ˜",raised_hands_tone2="๐Ÿ™Œ๐Ÿผ",fearful="๐Ÿ˜จ",grinning="๐Ÿ˜€",bow_tone5="๐Ÿ™‡๐Ÿฟ",santa_tone3="๐ŸŽ…๐Ÿฝ",santa_tone2="๐ŸŽ…๐Ÿผ",santa_tone5="๐ŸŽ…๐Ÿฟ",santa_tone4="๐ŸŽ…๐Ÿพ",bow_tone2="๐Ÿ™‡๐Ÿผ",bow_tone3="๐Ÿ™‡๐Ÿฝ",bathtub="๐Ÿ›",ping_pong="๐Ÿ“",u5272="๐Ÿˆน",rooster="๐Ÿ“",vs="๐Ÿ†š",bullettrain_front="๐Ÿš…",airplane_small="๐Ÿ›ฉ",white_circle="โšช",balloon="๐ŸŽˆ",cross="โœ",princess_tone3="๐Ÿ‘ธ๐Ÿฝ",speaker="๐Ÿ”ˆ",zipper_mouth="๐Ÿค",u6307="๐Ÿˆฏ",whale="๐Ÿณ",pensive="๐Ÿ˜”",signal_strength="๐Ÿ“ถ",muscle="๐Ÿ’ช",rocket="๐Ÿš€",camel="๐Ÿซ",boot="๐Ÿ‘ข",flashlight="๐Ÿ”ฆ",spy_tone4="๐Ÿ•ต๐Ÿพ",spy_tone5="๐Ÿ•ต๐Ÿฟ",ski="๐ŸŽฟ",spy_tone3="๐Ÿ•ต๐Ÿฝ",musical_keyboard="๐ŸŽน",spy_tone1="๐Ÿ•ต๐Ÿป",rolling_eyes="๐Ÿ™„",clock1="๐Ÿ•",clock2="๐Ÿ•‘",clock3="๐Ÿ•’",clock4="๐Ÿ•“",clock5="๐Ÿ•”",clock6="๐Ÿ••",clock7="๐Ÿ•–",clock8="๐Ÿ•—",clock9="๐Ÿ•˜",doughnut="๐Ÿฉ",dancer_tone1="๐Ÿ’ƒ๐Ÿป",dancer_tone2="๐Ÿ’ƒ๐Ÿผ",dancer_tone3="๐Ÿ’ƒ๐Ÿฝ",candy="๐Ÿฌ",two_men_holding_hands="๐Ÿ‘ฌ",badminton="๐Ÿธ",bust_in_silhouette="๐Ÿ‘ค",writing_hand_tone2="โœ๐Ÿผ",sunflower="๐ŸŒป",["e-mail"]="๐Ÿ“ง",chains="โ›“",kissing_smiling_eyes="๐Ÿ˜™",fish_cake="๐Ÿฅ",no_pedestrians="๐Ÿšท",v_tone4="โœŒ๐Ÿพ",v_tone5="โœŒ๐Ÿฟ",v_tone1="โœŒ๐Ÿป",v_tone2="โœŒ๐Ÿผ",v_tone3="โœŒ๐Ÿฝ",arrow_backward="โ—€",clock12="๐Ÿ•›",clock10="๐Ÿ•™",clock11="๐Ÿ•š",sweat="๐Ÿ˜“",mountain_railway="๐Ÿšž",tongue="๐Ÿ‘…",black_square_button="๐Ÿ”ฒ",do_not_litter="๐Ÿšฏ",nose_tone4="๐Ÿ‘ƒ๐Ÿพ",nose_tone5="๐Ÿ‘ƒ๐Ÿฟ",nose_tone2="๐Ÿ‘ƒ๐Ÿผ",nose_tone3="๐Ÿ‘ƒ๐Ÿฝ",nose_tone1="๐Ÿ‘ƒ๐Ÿป",horse_racing_tone5="๐Ÿ‡๐Ÿฟ",horse_racing_tone4="๐Ÿ‡๐Ÿพ",horse_racing_tone3="๐Ÿ‡๐Ÿฝ",ok_hand="๐Ÿ‘Œ",horse_racing_tone1="๐Ÿ‡๐Ÿป",custard="๐Ÿฎ",rowboat="๐Ÿšฃ",white_sun_small_cloud="๐ŸŒค",flag_kr="๐Ÿ‡ฐ๐Ÿ‡ท",cricket="๐Ÿ",flag_kp="๐Ÿ‡ฐ๐Ÿ‡ต",flag_kw="๐Ÿ‡ฐ๐Ÿ‡ผ",flag_kz="๐Ÿ‡ฐ๐Ÿ‡ฟ",flag_ky="๐Ÿ‡ฐ๐Ÿ‡พ",construction="๐Ÿšง",flag_kg="๐Ÿ‡ฐ๐Ÿ‡ฌ",flag_ke="๐Ÿ‡ฐ๐Ÿ‡ช",flag_ki="๐Ÿ‡ฐ๐Ÿ‡ฎ",flag_kh="๐Ÿ‡ฐ๐Ÿ‡ญ",flag_kn="๐Ÿ‡ฐ๐Ÿ‡ณ",flag_km="๐Ÿ‡ฐ๐Ÿ‡ฒ",cake="๐Ÿฐ",flag_wf="๐Ÿ‡ผ๐Ÿ‡ซ",mortar_board="๐ŸŽ“",pig="๐Ÿท",flag_ws="๐Ÿ‡ผ๐Ÿ‡ธ",person_frowning="๐Ÿ™",arrow_upper_right="โ†—",book="๐Ÿ“–",clock1130="๐Ÿ•ฆ",boom="๐Ÿ’ฅ",["repeat"]="๐Ÿ”",star="โญ",rabbit2="๐Ÿ‡",footprints="๐Ÿ‘ฃ",ghost="๐Ÿ‘ป",droplet="๐Ÿ’ง",vibration_mode="๐Ÿ“ณ",flag_ye="๐Ÿ‡พ๐Ÿ‡ช",flag_yt="๐Ÿ‡พ๐Ÿ‡น",
+}
+emoji['slightly_smiling_face'] = ':)'
+
+local function str2emoji(str)
+ if not str then return '' end
+ return (str:gsub(':[a-zA-Z0-9%-_]+:', function(word)
+ return emoji[word:match(':(.+):')] or word
+ end))
+end
+
+function emoji_replace_input_string(buffer)
+ -- Get input contents
+ local input_s = w.buffer_get_string(buffer, 'input')
+ -- Skip modification of settings
+ if input_s:match('^/set ') then
+ return w.WEECHAT_RC_OK
+ end
+ w.buffer_set(buffer, 'input', str2emoji(input_s))
+ return w.WEECHAT_RC_OK
+end
+
+function emoji_input_replacer(data, buffer, command)
+ if command == '/input return' then
+ return emoji_replace_input_string(buffer)
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function emoji_live_input_replace(data, modifier, modifier_data, msg)
+ return str2emoji(msg)
+end
+
+function emoji_out_replace(data, modifier, modifier_data, msg)
+ return str2emoji(msg)
+end
+
+function unshortcode_cb(data, modifier, modifier_data, msg)
+ return str2emoji(msg)
+end
+
+function emoji_complete_next_cb(data, buffer, command)
+ local input_s = w.buffer_get_string(buffer, 'input')
+ -- Require : in word
+ if not input_s:match(':') then
+ return w.WEECHAT_RC_OK
+ end
+ local current_pos = w.buffer_get_integer(buffer, "input_pos") - 1
+ --local input_length = w.buffer_get_integer(buffer, "input_length")
+ while current_pos >= 1 and input_s.sub(current_pos, current_pos) ~= ':' do
+ current_pos = current_pos - 1
+ end
+ if current_pos < 1 then
+ current_pos = 1
+ end
+
+ -- TODO: Support non-end-word editing
+ local oword = input_s:sub(current_pos)
+ local word = oword:match(':(.*)')
+ for e, b in pairs(emoji) do
+ if e:match(word) then
+ local new = (input_s:gsub(":"..word, b))
+ w.buffer_set(buffer, 'input', new)
+ --w.buffer_set(buffer, "input_pos", str(w.buffer_get_integer(buffer, "input_pos") + 1))
+ return w.WEECHAT_RC_OK_EAT
+ end
+ end
+
+ return w.WEECHAT_RC_OK
+end
+
+function emoji_completion_cb(data, completion_item, buffer, completion)
+ for k, v in pairs(emoji) do
+ w.hook_completion_list_add(completion, ":"..k..":", 0, w.WEECHAT_LIST_POS_SORT)
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function incoming_cb(data, modifier, modifier_data, msg)
+ --local plugin, buffer_name, tags = modifier_data:match('^(.-);(.-);(.-)$')
+ -- Only replace in incoming "messages"
+ if modifier_data:match('nick_') then
+ return str2emoji(msg)
+ end
+ return msg
+end
+
+function e_init()
+ if w.register(
+ SCRIPT_NAME,
+ SCRIPT_AUTHOR,
+ SCRIPT_VERSION,
+ SCRIPT_LICENSE,
+ SCRIPT_DESC,
+ '',
+ '') then
+ -- Hook input enter
+ w.hook_command_run('/input return', 'emoji_input_replacer', '')
+
+ -- Hook irc out for relay clients
+ --w.hook_modifier('input_text_for_buffer', 'emoji_out_replace', '')
+ w.hook_modifier('irc_out1_PRIVMSG', 'emoji_out_replace', '')
+ -- Replace while typing
+ w.hook_modifier('input_text_display_with_cursor', 'emoji_live_input_replace', '')
+ -- Hook tab complete
+ w.hook_command_run("/input complete_next", "emoji_complete_next_cb", "")
+ -- Hook for working togheter with other scripts
+ w.hook_modifier('emoji_unshortcode', 'unshortcode_cb', '')
+
+ w.hook_completion("emojis", "complete :emoji:s", "emoji_completion_cb", "")
+ local settings = {
+ incoming = {'on', 'Also try to replace shortcodes to emoji in incoming messages'}
+ }
+ -- set default settings
+ local version = tonumber(w.info_get('version_number', '') or 0)
+ for option, value in pairs(settings) do
+ if w.config_is_set_plugin(option) ~= 1 then
+ w.config_set_plugin(option, value[1])
+ end
+ if version >= 0x00030500 then
+ w.config_set_desc_plugin(option, ('%s (default: "%s")'):format(
+ value[2], value[1]))
+ end
+ end
+ -- Hook incoming message
+ if w.config_get_plugin('incoming') == 'on' then
+ w.hook_modifier("weechat_print", 'incoming_cb', '')
+ end
+ end
+end
+
+e_init()
diff --git a/weechat/lua/matrix.lua b/weechat/lua/matrix.lua
new file mode 100644
index 0000000..2ce0a2c
--- /dev/null
+++ b/weechat/lua/matrix.lua
@@ -0,0 +1,3354 @@
+-- WeeChat Matrix.org Client
+-- vim: expandtab:ts=4:sw=4:sts=4
+-- luacheck: globals weechat command_help command_connect matrix_command_cb matrix_away_command_run_cb configuration_changed_cb real_http_cb matrix_unload http_cb upload_cb send buffer_input_cb poll polltimer_cb cleartyping otktimer_cb join_command_cb part_command_cb leave_command_cb me_command_cb topic_command_cb upload_command_cb query_command_cb create_command_cb createalias_command_cb invite_command_cb list_command_cb op_command_cb voice_command_cb devoice_command_cb kick_command_cb deop_command_cb nick_command_cb whois_command_cb notice_command_cb msg_command_cb encrypt_command_cb public_command_cb names_command_cb more_command_cb roominfo_command_cb name_command_cb closed_matrix_buffer_cb closed_matrix_room_cb typing_notification_cb buffer_switch_cb typing_bar_item_cb
+
+--[[
+ Author: xt <xt@xt.gg>
+ Thanks to Ryan Huber of wee_slack.py for some ideas and inspiration.
+
+ This script is considered alpha quality as only the bare minimal of
+ functionality is in place and it is not very well tested.
+
+ It is known to be able to crash WeeChat in certain scenarioes so all
+ usage of this script is at your own risk.
+
+ If at any point there seems to be problem, make sure you update to
+ the latest version of this script. You can also try reloading the
+ script using /lua reload matrix to refresh all the state.
+
+Power Levels
+------------
+
+A default Matrix room has power level between 0 to 100.
+This script maps this as follows:
+
+ ~ Room creator
+ & Power level 100
+ @ Power level 50
+ + Power level > 0
+
+ TODO
+ ----
+ /ban
+ Giving people arbitrary power levels
+ Lazyload messages instead of HUGE initialSync
+ Dynamically fetch more messages in backlog when user reaches the
+ oldest message using pgup
+ Need a way to change room join rule
+ Fix broken state after failed initial connect
+ Fix parsing of multiple join messages
+ Friendlier error message on bad user/password
+ Parse some HTML and turn into color/bold/etc
+ Support weechat.look.prefix_same_nick
+
+]]
+
+local json = require 'cjson' -- apt-get install lua-cjson
+local olmstatus, olm = pcall(require, 'olm') -- LuaJIT olm FFI binding ln -s ~/olm/olm.lua /usr/local/share/lua/5.1
+local w = weechat
+
+local SCRIPT_NAME = "matrix"
+local SCRIPT_AUTHOR = "xt <xt@xt.gg>"
+local SCRIPT_VERSION = "3"
+local SCRIPT_LICENSE = "MIT"
+local SCRIPT_DESC = "Matrix.org chat plugin"
+local SCRIPT_COMMAND = SCRIPT_NAME
+
+local WEECHAT_VERSION
+
+local SERVER
+local STDOUT = {}
+local OUT = {}
+local BUFFER
+local Room
+local MatrixServer
+local Olm
+local DEBUG = false
+-- How many seconds to timeout if nothing happened on the server. If something
+-- happens before it will return sooner.
+-- default Nginx proxy timeout is 60s, so we go slightly lower
+local POLL_INTERVAL = 55
+
+local default_color = w.color('default')
+-- Cache error variables so we don't have to look them up for every error
+-- message, a normal user will not change these ever anyway.
+local errprefix
+local errprefix_c
+
+local HOMEDIR
+local OLM_ALGORITHM = 'm.olm.v1.curve25519-aes-sha2'
+local OLM_KEY = 'secr3t' -- TODO configurable using weechat sec data
+local v2_api_ns = '_matrix/client/v2_alpha'
+
+local function tprint(tbl, indent, out)
+ if not indent then indent = 0 end
+ if not out then out = BUFFER end
+ for k, v in pairs(tbl) do
+ local formatting = string.rep(" ", indent) .. k .. ": "
+ if type(v) == "table" then
+ w.print(out, formatting)
+ tprint(v, indent+1, out)
+ elseif type(v) == 'boolean' then
+ w.print(out, formatting .. tostring(v))
+ elseif type(v) == 'userdata' then
+ w.print(out, formatting .. tostring(v))
+ else
+ w.print(out, formatting .. v)
+ end
+ end
+end
+
+local function mprint(message)
+ -- Print message to matrix buffer
+ if type(message) == 'table' then
+ tprint(message)
+ else
+ message = tostring(message)
+ w.print(BUFFER, message)
+ end
+end
+
+local function perr(message)
+ if message == nil then return end
+ -- Print error message to the matrix "server" buffer using WeeChat styled
+ -- error message
+ mprint(
+ errprefix_c ..
+ errprefix ..
+ '\t' ..
+ default_color ..
+ tostring(message)
+ )
+end
+
+local function dbg(message)
+ perr('- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -')
+ if type(message) == 'table' then
+ tprint(message)
+ else
+ message = ("DEBUG\t%s"):format(tostring(message))
+ mprint(BUFFER, message)
+ end
+end
+
+local dtraceback = debug.traceback
+-- luacheck: ignore debug
+debug.traceback = function (...)
+ if select('#', ...) >= 1 then
+ local err, lvl = ...
+ local trace = dtraceback(err, (lvl or 2)+1)
+ perr(trace)
+ end
+ -- direct call to debug.traceback: return the original.
+ -- debug.traceback(nil, level) doesn't work in Lua 5.1
+ -- (http://lua-users.org/lists/lua-l/2011-06/msg00574.html), so
+ -- simply remove first frame from the stack trace
+ return (dtraceback(...):gsub("(stack traceback:\n)[^\n]*\n", "%1"))
+end
+
+local function weechat_eval(text)
+ if WEECHAT_VERSION >= 0x00040200 then
+ return w.string_eval_expression(text,{},{},{})
+ end
+ return text
+end
+
+local urllib = {}
+urllib.quote = function(str)
+ if not str then return '' end
+ if type(str) == 'number' then return str end
+ return str:gsub(
+ '([^%w ])',
+ function (c)
+ return string.format ("%%%02X", string.byte(c))
+ end
+ ):gsub(' ', '+')
+end
+urllib.urlencode = function(tbl)
+ local out = {}
+ for k, v in pairs(tbl) do
+ table.insert(out, urllib.quote(k)..'='..urllib.quote(v))
+ end
+ return table.concat(out, '&')
+end
+
+local function accesstoken_redact(str)
+ return (str:gsub('access.*token=[0-9a-zA-Z%%]*', 'access_token=[redacted]'))
+end
+
+local transaction_id_counter = 0
+local function get_next_transaction_id()
+ transaction_id_counter = transaction_id_counter + 1
+ return transaction_id_counter
+end
+
+--[[
+-- Function for signing json, unused for now, we hand craft the required
+-- signed json in the encryption function. But I think this function will be
+-- needed in the future so leaving it here in a commented version
+local function sign_json(json_object, signing_key, signing_name)
+ -- See: https://github.com/matrix-org/matrix-doc/blob/master/specification/31_event_signing.rst
+ -- Maybe use:http://regex.info/code/JSON.lua which sorts keys
+ local signatures = json_object.signatures or {}
+ json_object.signatures = nil
+
+ local unsigned = json_object.unsigned or nil
+ json_object.unsigned = nil
+
+ -- TODO ensure canonical json
+ local signed = signing_key:sign(json.encode(json_object))
+ local signature_base64 = encode_base64(signed.signature)
+
+ local key_id = ("%s:%s"):format(signing_key.agl, signing_key.version)
+ signatures[signing_name] = {[key_id] = signature_base64}
+
+ json_object.signatures = signatures
+ if unsigned then
+ json_object.unsigned = unsigned
+ end
+
+ return json_object
+end
+--]]
+
+local function split_args(args)
+ local function_name, arg = args:match('^(.-) (.*)$')
+ return function_name, arg
+end
+
+local function byte_to_tag(s, byte, open_tag, close_tag)
+ if s:match(byte) then
+ local inside = false
+ local open_tags = 0
+ local htmlbody = s:gsub(byte, function(c)
+ if inside then
+ inside = false
+ return close_tag
+ end
+ inside = true
+ open_tags = open_tags + 1
+ return open_tag
+ end)
+ local _, count = htmlbody:gsub(close_tag, '')
+ -- Ensure we close tags
+ if count < open_tags then
+ htmlbody = htmlbody .. close_tag
+ end
+ return htmlbody
+ end
+ return s
+end
+
+local function irc_formatting_to_html(s)
+ -- TODO, support foreground and background?
+ local ct = {'white','black','blue','green','red','maroon','purple',
+ 'orange','yellow','lightgreen','teal','cyan', 'lightblue',
+ 'fuchsia', 'gray', 'lightgray'}
+
+ s = byte_to_tag(s, '\02', '<em>', '</em>')
+ s = byte_to_tag(s, '\029', '<i>', '</i>')
+ s = byte_to_tag(s, '\031', '<u>', '</u>')
+ -- First do full color strings with reset.
+ -- Iterate backwards to catch long colors before short
+ for i=#ct,1,-1 do
+ s = s:gsub(
+ '\003'..tostring(i-1)..'(.-)\003',
+ '<font color="'..ct[i]..'">%1</font>')
+ end
+
+ -- Then replace unmatch colors
+ -- Iterate backwards to catch long colors before short
+ for i=#ct,1,-1 do
+ local c = ct[i]
+ s = byte_to_tag(s, '\003'..tostring(i-1),
+ '<font color="'..c..'">', '</font>')
+ end
+ return s
+end
+
+local function strip_irc_formatting(s)
+ if not s then return '' end
+ return (s
+ :gsub("\02", "")
+ :gsub("\03%d%d?,%d%d?", "")
+ :gsub("\03%d%d?", "")
+ :gsub("\03", "")
+ :gsub("\15", "")
+ :gsub("\17", "")
+ :gsub("\18", "")
+ :gsub("\22", "")
+ :gsub("\29", "")
+ :gsub("\31", ""))
+end
+
+local function irc_formatting_to_weechat_color(s)
+ -- TODO, support foreground and background?
+ -- - is atribute to remove formatting
+ -- | is to keep formatting during color changes
+ s = byte_to_tag(s, '\02', w.color'bold', w.color'-bold')
+ s = byte_to_tag(s, '\029', w.color'italic', w.color'-italic')
+ s = byte_to_tag(s, '\031', w.color'underline', w.color'-underline')
+ -- backwards to catch long numbers before short
+ for i=16,1,-1 do
+ i = tostring(i)
+ s = byte_to_tag(s, '\003'..i,
+ w.color("|"..i), w.color("-"..i))
+ end
+ return s
+end
+
+function matrix_unload()
+ w.print('', 'matrix: Unloading')
+ -- Clear/free olm memory if loaded
+ if olmstatus then
+ w.print('', 'matrix: Saving olm state')
+ SERVER.olm:save()
+ w.print('', 'matrix: Clearing olm state from memory')
+ SERVER.olm.account:clear()
+ --SERVER.olm = nil
+ end
+ w.print('', 'matrix: done cleaning up!')
+ return w.WEECHAT_RC_OK
+end
+
+local function wconf(optionname)
+ return w.config_string(w.config_get(optionname))
+end
+
+local function wcolor(optionname)
+ return w.color(wconf(optionname))
+end
+
+function command_help(current_buffer, args)
+ if args then
+ local help_cmds = {args=args}
+ if not help_cmds then
+ w.print("", "Command not found: " .. args)
+ return
+ end
+ for cmd, helptext in pairs(help_cmds) do
+ w.print('', w.color("bold") .. cmd)
+ w.print('', (helptext or 'No help text').strip())
+ w.print('', '')
+ end
+ end
+
+end
+
+function command_connect(current_buffer, args)
+ if not SERVER.connected then
+ SERVER:connect()
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function matrix_command_cb(data, current_buffer, args)
+ if args == 'connect' then
+ return command_connect(current_buffer)
+ elseif args == 'debug' then
+ if DEBUG then
+ DEBUG = false
+ w.print('', SCRIPT_NAME..': debugging messages disabled')
+ else
+ DEBUG = true
+ w.print('', SCRIPT_NAME..': debugging messages enabled')
+ end
+ elseif args:match('^msg ') then
+ local _
+ _, args = split_args(args) -- remove cmd
+ local roomarg, msg = split_args(args)
+ local room
+ for id, r in pairs(SERVER.rooms) do
+ -- Send /msg to a ID
+ if id == roomarg then
+ room = r
+ break
+ elseif roomarg == r.name then
+ room = r
+ break
+ elseif roomarg == r.roomname then
+ room = r
+ break
+ end
+ end
+
+ if room then
+ room:Msg(msg)
+ return w.WEECHAT_RC_OK_EAT
+ end
+ else
+ perr("Command not found: " .. args)
+ end
+
+ return w.WEECHAT_RC_OK
+end
+
+function matrix_away_command_run_cb(data, buffer, args)
+ -- Callback for command /away -all
+ local _
+ _, args = split_args(args) -- remove cmd
+ local msg
+ _, msg = split_args(args)
+ w.buffer_set(BUFFER, "localvar_set_away", msg)
+ for id, room in pairs(SERVER.rooms) do
+ if msg and msg ~= '' then
+ w.buffer_set(room.buffer, "localvar_set_away", msg)
+ else
+ -- Delete takes empty string, and not nil
+ w.buffer_set(room.buffer, "localvar_del_away", '')
+ end
+ end
+ if msg and msg ~= '' then
+ SERVER:SendPresence('unavailable', msg)
+ mprint 'You have been marked as unavailable'
+ else
+ SERVER:SendPresence('online', nil)
+ mprint 'You have been marked as online'
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function configuration_changed_cb(data, option, value)
+ if value == 'on' then
+ DEBUG = true
+ w.print('', SCRIPT_NAME..': debugging messages enabled')
+ else
+ DEBUG = false
+ w.print('', SCRIPT_NAME..': debugging messages disabled')
+ end
+end
+
+local function http(url, post, cb, timeout, extra, api_ns)
+ if not post then
+ post = {}
+ end
+ if not cb then
+ cb = 'http_cb'
+ end
+ if not timeout then
+ timeout = 60*1000
+ end
+ if not extra then
+ extra = nil
+ end
+ if not api_ns then
+ api_ns = "_matrix/client/r0"
+ end
+
+ -- Add accept encoding by default if it's not already there
+ if not post.accept_encoding then
+ post.accept_encoding = 'application/json'
+ end
+ if not post.header then
+ post.header = 1 -- Request http headers in the response
+ end
+
+ if not url:match'https?://' then
+ local homeserver_url = w.config_get_plugin('homeserver_url')
+ homeserver_url = homeserver_url .. api_ns
+ url = homeserver_url .. url
+ end
+
+ if DEBUG then
+ dbg{request={
+ url=accesstoken_redact(url),
+ post=post,extra=extra}
+ }
+ end
+ w.hook_process_hashtable('url:' .. url, post, timeout, cb, extra)
+end
+
+local function parse_http_statusline(line)
+ local httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.-)\r?\n")
+ if not httpversion then
+ return
+ end
+ return httpversion, tonumber(status_code), reason_phrase
+end
+
+function real_http_cb(extra, command, rc, stdout, stderr)
+ if DEBUG then
+ dbg{reply={
+ command=accesstoken_redact(command),
+ extra=extra,rc=rc,stdout=stdout,stderr=stderr}
+ }
+ end
+
+ if stderr and stderr ~= '' then
+ mprint(('error: %s'):format(stderr))
+ return w.WEECHAT_RC_OK
+ end
+
+ -- Because of a bug in WeeChat sometimes the stdout gets prepended by
+ -- any number of BEL chars (hex 07). Let's have a nasty workaround and
+ -- just replace them away.
+ if WEECHAT_VERSION < 0x01030000 then -- fixed in 1.3
+ stdout = (stdout:gsub('^\007*', ''))
+ end
+
+ if stdout ~= '' then
+ if not STDOUT[command] then
+ STDOUT[command] = {}
+ end
+ table.insert(STDOUT[command], stdout)
+ end
+
+ if tonumber(rc) >= 0 then
+ stdout = table.concat(STDOUT[command] or {})
+ STDOUT[command] = nil
+ local httpversion, status_code, reason_phrase = parse_http_statusline(stdout)
+ if not httpversion then
+ perr(('Invalid http request: %s'):format(stdout))
+ return w.WEECHAT_RC_OK
+ end
+ if status_code >= 500 then
+ perr(('HTTP API returned error. Code: %s, reason: %s'):format(status_code, reason_phrase))
+ return w.WEECHAT_RC_OK
+ end
+ -- Skip to data
+ stdout = (stdout:match('.-\r?\n\r?\n(.*)'))
+ -- Protected call in case of JSON errors
+ local success, js = pcall(json.decode, stdout)
+ if not success then
+ mprint(('error\t%s during json load: %s'):format(js, stdout))
+ return w.WEECHAT_RC_OK
+ end
+ if js['errcode'] or js['error'] then
+ if command:find'login' then
+ w.print('', ('matrix: Error code during login: %s, code: %s'):format(
+ js['error'], js['errcode']))
+ w.print('', 'matrix: Please verify your username and password')
+ else
+ perr('API call returned error: '..js['error'] .. '('..tostring(js.errcode)..')')
+ end
+ return w.WEECHAT_RC_OK
+ end
+ -- Get correct handler
+ if command:find('login') then
+ for k, v in pairs(js) do
+ SERVER[k] = v
+ end
+ SERVER.connected = true
+ SERVER:initial_sync()
+ elseif command:find'/rooms/.*/initialSync' then
+ local myroom = SERVER:addRoom(js)
+ for _, chunk in ipairs(js['presence']) do
+ myroom:ParseChunk(chunk, true, 'presence')
+ end
+ for _, chunk in ipairs(js['messages']['chunk']) do
+ myroom:ParseChunk(chunk, true, 'messages')
+ end
+ elseif command:find'/sync' then
+ SERVER.end_token = js.next_batch
+
+ -- We have a new end token, which means we safely can release the
+ -- poll lock
+ SERVER.poll_lock = false
+
+ local backlog = false
+ local initial = false
+ if extra == 'initial' then
+ initial = true
+ backlog = true
+ end
+
+ -- Start with setting the global presence variable on the server
+ -- so when the nicks get added to the room they can get added to
+ -- the correct nicklist group according to if they have presence
+ -- or not
+ for _, e in ipairs(js.presence.events) do
+ SERVER:UpdatePresence(e)
+ end
+ for membership, rooms in pairs(js['rooms']) do
+ -- If we left the room, simply ignore it
+ if membership ~= 'leave' or (membership == 'leave' and (not backlog)) then
+ for identifier, room in pairs(rooms) do
+ -- Monkey patch it to look like v1 object
+ room.room_id = identifier
+ local myroom
+ if initial then
+ myroom = SERVER:addRoom(room)
+ else
+ myroom = SERVER.rooms[identifier]
+ -- Chunk for non-existing room
+ if not myroom then
+ myroom = SERVER:addRoom(room)
+ if not membership == 'invite' then
+ perr('Event for unknown room')
+ end
+ end
+ end
+ -- First of all parse invite states.
+ local inv_states = room.invite_state
+ if inv_states then
+ local chunks = room.invite_state.events or {}
+ for _, chunk in ipairs(chunks) do
+ myroom:ParseChunk(chunk, backlog, 'states')
+ end
+ end
+ -- Parse states before messages so we can add nicks and stuff
+ -- before messages start appearing
+ local states = room.state
+ if states then
+ local chunks = room.state.events or {}
+ for _, chunk in ipairs(chunks) do
+ myroom:ParseChunk(chunk, backlog, 'states')
+ end
+ end
+ local timeline = room.timeline
+ if timeline then
+ -- Save the prev_batch on the initial message so we
+ -- know for later when we picked up the sync
+ if initial then
+ myroom.prev_batch = timeline.prev_batch
+ end
+ local chunks = timeline.events or {}
+ for _, chunk in ipairs(chunks) do
+ myroom:ParseChunk(chunk, backlog, 'messages')
+ end
+ end
+ local ephemeral = room.ephemeral
+ -- Ignore Ephemeral Events during initial sync
+ if (extra and extra ~= 'initial') and ephemeral then
+ local chunks = ephemeral.events or {}
+ for _, chunk in ipairs(chunks) do
+ myroom:ParseChunk(chunk, backlog, 'states')
+ end
+ end
+ if backlog then
+ -- All the state should be done. Try to get a good name for the room now.
+ myroom:SetName(myroom.identifier)
+ end
+ end
+ end
+ end
+ -- Now we have created rooms and can go over the rooms and update
+ -- the presence for each nick
+ for _, e in pairs(js.presence.events) do
+ SERVER:UpdatePresence(e)
+ end
+ if initial then
+ SERVER:post_initial_sync()
+ end
+ SERVER:poll()
+ elseif command:find'messages' then
+ local identifier = extra
+ local myroom = SERVER.rooms[identifier]
+ myroom.prev_batch = js['end']
+ -- Freeze buffer
+ myroom:Freeze()
+ -- Clear buffer
+ myroom:Clear()
+ -- We request backwards direction, so iterate backwards
+ for i=#js.chunk,1,-1 do
+ local chunk = js.chunk[i]
+ myroom:ParseChunk(chunk, true, 'messages')
+ end
+ -- Thaw!
+ myroom:Thaw()
+ elseif command:find'/join/' then
+ -- We came from a join command, fecth some messages
+ local found = false
+ for id, _ in pairs(SERVER.rooms) do
+ if id == js.room_id then
+ found = true
+ -- this is a false positive for example when getting
+ -- invited. need to investigate more
+ --mprint('error\tJoined room, but already in it.')
+ break
+ end
+ end
+ if not found then
+ local data = urllib.urlencode({
+ access_token= SERVER.access_token,
+ --limit= w.config_get_plugin('backlog_lines'),
+ limit = 10,
+ })
+ http(('/rooms/%s/initialSync?%s'):format(
+ urllib.quote(js.room_id), data))
+ end
+ elseif command:find'leave' then
+ -- We store room_id in extra
+ local room_id = extra
+ SERVER:delRoom(room_id)
+ elseif command:find'/keys/claim' then
+ local count = 0
+ for user_id, v in pairs(js.one_time_keys or {}) do
+ for device_id, keys in pairs(v or {}) do
+ for key_id, key in pairs(keys or {}) do
+ SERVER.olm.otks[user_id..':'..device_id] = {[device_id]=key}
+ perr(('olm: Recieved OTK for user %s for device id %s'):format(user_id, device_id))
+ count = count + 1
+ SERVER.olm:create_session(user_id, device_id)
+ end
+ end
+ end
+ elseif command:find'/keys/query' then
+ for k, v in pairs(js.device_keys or {}) do
+ SERVER.olm.device_keys[k] = v
+
+ -- Claim keys for all only if missing session
+ for device_id, device_data in pairs(v) do
+ -- First try to create session from saved data
+ -- if that doesn't success we will download otk
+ local device_key = device_data.keys['curve25519:'..device_id]
+ local sessions = SERVER.olm:get_sessions(device_key)
+ if #sessions == 0 then
+ perr('olm: Downloading otk for user '..k..', and device_id: '..device_id)
+ SERVER.olm:claim(k, device_id)
+ else
+ perr('olm: Reusing existing session for user '..k)
+ end
+ end
+ end
+ elseif command:find'/keys/upload' then
+ local key_count = 0
+ local sensible_number_of_keys = 20
+ for algo, count in pairs(js.one_time_key_counts) do
+ key_count = count
+ SERVER.olm.key_count = key_count
+ end
+ if DEBUG then
+ perr('olm: Number of own OTKs uploaded to server: '..key_count)
+ end
+ -- TODO make endless loop prevention in case of server error
+ if key_count < sensible_number_of_keys then
+ SERVER.olm:upload_keys()
+ end
+ elseif command:find'upload' then
+ -- We store room_id in extra
+ local room_id = extra
+ if js.content_uri then
+ SERVER:Msg(room_id, js.content_uri)
+ end
+ -- luacheck: ignore 542
+ elseif command:find'/typing/' then
+ -- either it errs or it is empty
+ elseif command:find'/state/' then
+ -- TODO errorcode: M_FORBIDDEN
+ -- either it errs or it is empty
+ --dbg({state= js})
+ elseif command:find'/send/' then
+ -- XXX Errorhandling
+ -- TODO save event id to use for localecho
+ local event_id = js.event_id
+ local room_id = extra
+ -- When using relay client, WeeChat doesn't get any buffer_switch
+ -- signals, and thus cannot know when the relay client reads any
+ -- messages. https://github.com/weechat/weechat/issues/180
+ -- As a better than nothing approach we send read receipt when
+ -- user sends a message, since most likely the user has read
+ -- messages in that room if sending messages to it.
+ SERVER:SendReadReceipt(room_id, event_id)
+ elseif command:find'createRoom' then
+ -- We get join events, so we don't have to do anything
+ elseif command:find'/publicRooms' then
+ mprint 'Public rooms:'
+ mprint '\tUsers\tName\tTopic\tAliases'
+ table.sort(js.chunk, function(a, b)
+ return a.num_joined_members > b.num_joined_members
+ end)
+ for _, r in ipairs(js.chunk) do
+ local name = ''
+ if r.name and r.name ~= json.null then
+ name = r.name:gsub('\n', '')
+ end
+ local topic = ''
+ if r.topic and r.topic ~= json.null then
+ topic = r.topic:gsub('\n', '')
+ end
+ mprint(('%s %s %s %s')
+ :format(
+ r.num_joined_members or '',
+ name or '',
+ topic or '',
+ table.concat(r.aliases or {}, ', ')))
+ end
+ -- luacheck: ignore 542
+ elseif command:find'/invite' then
+ elseif command:find'receipt' then
+ -- we don't care about receipts for now
+ elseif command:find'directory/room' then
+ --- XXX: parse result
+ mprint 'Created new alias for room'
+ elseif command:match'presence/.*/status' then
+ -- Return of SendPresence which we don't have to handle because
+ -- it will be sent back to us as an event
+ else
+ dbg{['error'] = {msg='Unknown command in http cb', command=command,
+ js=js}}
+ end
+ end
+
+ if tonumber(rc) == -2 then -- -2 == WEECHAT_HOOK_PROCESS_ERROR
+ perr(('Call to API errored in command %s, maybe timeout?'):format(
+ accesstoken_redact(command)))
+ -- Empty cache in case of errors
+ if STDOUT[command] then
+ STDOUT[command] = nil
+ end
+ -- Release poll lock in case of errors
+ SERVER.poll_lock = false
+ end
+
+ return w.WEECHAT_RC_OK
+end
+
+function http_cb(data, command, rc, stdout, stderr)
+ local status, result = pcall(real_http_cb, data, command, rc, stdout, stderr)
+ if not status then
+ perr('Error in http_cb: ' .. tostring(result))
+ perr(debug.traceback())
+ end
+ return result
+end
+
+function upload_cb(data, command, rc, stdout, stderr)
+ local success, js = pcall(json.decode, stdout)
+ if not success then
+ mprint(('error\t%s when getting uploaded URI: %s'):format(js, stdout))
+ return w.WEECHAT_RC_OK
+ end
+
+ local uri = js.content_uri
+ if not uri then
+ mprint(('error\tNo content_uri after upload. Stdout: %s, stderr: %s'):format(stdout, stderr))
+ return w.WEECHAT_RC_OK
+ end
+
+ local room_id = data
+ local body = 'Image'
+ local msgtype = 'm.image'
+ SERVER:Msg(room_id, body, msgtype, uri)
+
+ return w.WEECHAT_RC_OK
+end
+
+Olm = {}
+Olm.__index = Olm
+Olm.create = function()
+ local olmdata = {}
+ setmetatable(olmdata, Olm)
+ if not olmstatus then
+ w.print('', SCRIPT_NAME .. ': Unable to load olm encryption library. Not enabling encryption. Please see documentation (README.md) for information on how to enable.')
+ return
+ end
+
+ local account = olm.Account.new()
+ olmdata.account = account
+ olmdata.sessions = {}
+ olmdata.device_keys = {}
+ olmdata.otks = {}
+ -- Try to read account from filesystem, if not generate a new account
+ local fd = io.open(HOMEDIR..'account.olm', 'rb')
+ local pickled = ''
+ if fd then
+ pickled = fd:read'*all'
+ fd:close()
+ end
+ if pickled == '' then
+ account:create()
+ local _, err = account:generate_one_time_keys(5)
+ perr(err)
+ else
+ local _, err = account:unpickle(OLM_KEY, pickled)
+ perr(err)
+ end
+ local identity = json.decode(account:identity_keys())
+ -- TODO figure out what device id is supposed to be
+ olmdata.device_id = identity.ed25519:match'%w*' -- problems with nonalfanum
+ olmdata.device_key = identity.curve25519
+ w.print('', 'matrix: Encryption loaded. To send encrypted messages in a room, use command /encrypt on with a room as active current buffer')
+ if DEBUG then
+ dbg{olm={
+ 'Loaded identity:',
+ json.decode(account:identity_keys())
+ }}
+ end
+ return olmdata
+end
+
+function Olm:save()
+ -- Save account and every pickled session
+ local pickle, err = self.account:pickle(OLM_KEY)
+ perr(err)
+ local fd = io.open(HOMEDIR..'account.olm', 'wb')
+ fd:write(pickle)
+ fd:close()
+ --for key, pickled in pairs(self.sessions) do
+ -- local user_id, device_id = key:match('(.*):(.+)')
+ -- self.write_session_to_file(pickled, user_id, device_id)
+ --end
+end
+
+function Olm:query(user_ids) -- Query keys from other user_id
+ if DEBUG then
+ perr('olm: querying user_ids')
+ tprint(user_ids)
+ end
+ local auth = urllib.urlencode{access_token=SERVER.access_token}
+ local data = {
+ device_keys = {}
+ }
+ for _, uid in pairs(user_ids) do
+ data.device_keys[uid] = {false}
+ end
+ http('/keys/query/?'..auth,
+ {postfields=json.encode(data)},
+ 'http_cb',
+ 5*1000, nil,
+ v2_api_ns
+ )
+end
+
+function Olm:check_server_keycount()
+ local data = urllib.urlencode{access_token=SERVER.access_token}
+ http('/keys/upload/'..self.device_id..'?'..data,
+ {},
+ 'http_cb', 5*1000, nil, v2_api_ns
+ )
+end
+
+function Olm:upload_keys()
+ if DEBUG then
+ perr('olm: Uploading keys')
+ end
+ local id_keys = json.decode(self.account:identity_keys())
+ local user_id = SERVER.user_id
+ local one_time_keys = {}
+ local otks = json.decode(self.account:one_time_keys())
+ local keyCount = 0
+ for id, k in pairs(otks.curve25519) do
+ keyCount = keyCount + 1
+ end
+ perr('olm: keycount: '..tostring(keyCount))
+ if keyCount < 5 then -- try to always have 5 keys
+ perr('olm: newly generated keys: '..tostring(tonumber(
+ self.account:generate_one_time_keys(5 - keyCount))))
+ otks = json.decode(self.account:one_time_keys())
+ end
+
+ for id, key in pairs(otks.curve25519) do
+ one_time_keys['curve25519:'..id] = key
+ keyCount = keyCount + 1
+ end
+
+ -- Construct JSON manually so it's ready for signing
+ local keys_json = '{"algorithms":["' .. OLM_ALGORITHM .. '"]'
+ .. ',"device_id":"' .. self.device_id .. '"'
+ .. ',"keys":'
+ .. '{"ed25519:' .. self.device_id .. '":"'
+ .. id_keys.ed25519
+ .. '","curve25519:' .. self.device_id .. '":"'
+ .. id_keys.curve25519
+ .. '"}'
+ .. ',"user_id":"' .. user_id
+ .. '"}'
+
+ local success, key_data = pcall(json.decode, keys_json)
+ -- TODO save key data to device_keys so we don't have to download
+ -- our own keys from the servers?
+ if not success then
+ perr(('olm: upload_keys: %s when converting to json: %s')
+ :format(key_data, keys_json))
+ end
+
+ local msg = {
+ device_keys = key_data,
+ one_time_keys = one_time_keys
+ }
+ msg.device_keys.signatures = {
+ [user_id] = {
+ ["ed25519:"..self.device_id] = self.account:sign(keys_json)
+ }
+ }
+ local data = urllib.urlencode{
+ access_token = SERVER.access_token
+ }
+ http('/keys/upload/'..self.device_id..'?'..data, {
+ postfields = json.encode(msg)
+ }, 'http_cb', 5*1000, nil, v2_api_ns)
+
+ self.account:mark_keys_as_published()
+
+end
+
+function Olm:claim(user_id, device_id) -- Fetch one time keys
+ if DEBUG then
+ perr(('olm: Claiming OTK for user: %s and device: %s'):format(user_id, device_id))
+ end
+ -- TODO take a list of ids for batch downloading
+ local auth = urllib.urlencode{ access_token = SERVER.access_token }
+ local data = {
+ one_time_keys = {
+ [user_id] = {
+ [device_id] = 'curve25519'
+ }
+ }
+ }
+ http('/keys/claim?'..auth,
+ {postfields=json.encode(data)},
+ 'http_cb', 30*1000, nil, v2_api_ns
+ )
+end
+
+function Olm:create_session(user_id, device_id)
+ perr(('olm: creating session for user: %s, and device: %s'):format(user_id, device_id))
+ local device_data = self.device_keys[user_id][device_id]
+ if not device_data then
+ perr(('olm: missing device data for user: %s, and device: %s'):format(user_id, device_id))
+ return
+ end
+ local device_key = device_data.keys['curve25519:'..device_id]
+ if not device_key then
+ perr("olm: Missing key for user: "..user_id.." and device: "..device_id.."")
+ return
+ end
+ local sessions = self:get_sessions(device_key)
+ if not sessions[device_key] then
+ perr(('olm: creating NEW session for: %s, and device: %s'):format(user_id, device_id))
+ local session = olm.Session.new()
+ local otk = self.otks[user_id..':'..device_id]
+ if not otk then
+ perr("olm: Missing OTK for user: "..user_id.." and device: "..device_id.."")
+ else
+ otk = otk[device_id]
+ end
+ if otk then
+ session:create_outbound(self.account, device_key, otk)
+ local session_id = session:session_id()
+ perr('olm: Session ID:'..tostring(session_id))
+ self:store_session(device_key, session)
+ end
+ session:clear()
+ end
+end
+
+function Olm:get_sessions(device_key)
+ if DEBUG then
+ perr("olm: get_sessions: device: "..device_key.."")
+ end
+ local sessions = self.sessions[device_key]
+ if not sessions then
+ sessions = self:read_session(device_key)
+ end
+ return sessions
+end
+
+function Olm:read_session(device_key)
+ local session_filename = HOMEDIR..device_key..'.session.olm'
+ local fd, err = io.open(session_filename, 'rb')
+ if fd then
+ perr(('olm: reading saved session device: %s'):format(device_key))
+ local sessions = fd:read'*all'
+ sessions = json.decode(sessions)
+ self.sessions[device_key] = sessions
+ fd:close()
+ return sessions
+ else
+ perr(('olm: Error: %s, reading saved session device: %s'):format(err, device_key))
+ end
+ return {}
+end
+
+function Olm:store_session(device_key, session)
+ local session_id = session:session_id()
+ if DEBUG then
+ perr("olm: store_session: device: "..device_key..", Session ID: "..session_id)
+ end
+ local sessions = self.sessions[device_key] or {}
+ local pickled = session:pickle(OLM_KEY)
+ sessions[session_id] = pickled
+ self.sessions[device_key] = sessions
+ self:write_session_to_file(sessions, device_key)
+end
+
+function Olm:write_session_to_file(sessions, device_key)
+ local session_filename = HOMEDIR..device_key..'.session.olm'
+ local fd, err = io.open(session_filename, 'wb')
+ if fd then
+ fd:write(json.encode(sessions))
+ fd:close()
+ else
+ perr('olm: error saving session: '..tostring(err))
+ end
+end
+
+MatrixServer = {}
+MatrixServer.__index = MatrixServer
+
+MatrixServer.create = function()
+ local server = {}
+ setmetatable(server, MatrixServer)
+ server.nick = nil
+ server.connecting = false
+ server.connected = false
+ server.rooms = {}
+ -- Store user presences here since they are not local to the rooms
+ server.presence = {}
+ server.end_token = 'END'
+ server.typing_time = os.time()
+ server.typingtimer = w.hook_timer(10*1000, 0, 0, "cleartyping", "")
+
+ -- Use a lock to prevent multiple simul poll with same end token, which
+ -- could lead to duplicate messages
+ server.poll_lock = false
+ server.olm = Olm.create()
+ if server.olm then -- might not be available
+ -- Run save so we do not lose state. Create might create new account,
+ -- new keys, etc.
+ server.olm:save()
+ end
+ return server
+end
+
+function MatrixServer:UpdatePresence(c)
+ local user_id = c.sender or c.content.user_id
+ self.presence[user_id] = c.content.presence
+ for id, room in pairs(self.rooms) do
+ room:UpdatePresence(c.sender, c.content.presence)
+ end
+end
+
+function MatrixServer:findRoom(buffer_ptr)
+ for id, room in pairs(self.rooms) do
+ if room.buffer == buffer_ptr then
+ return room
+ end
+ end
+end
+
+function MatrixServer:connect()
+ if not self.connecting then
+ local user = weechat_eval(w.config_get_plugin('user'))
+ local password = weechat_eval(w.config_get_plugin('password'))
+ if user == '' or password == '' then
+ w.print('', 'Please set your username and password using the settings system and then type /matrix connect')
+ return
+ end
+
+ self.connecting = true
+ w.print('', 'matrix: Connecting to homeserver URL: '..
+ w.config_get_plugin('homeserver_url'))
+ local post = {
+ ["type"]="m.login.password",
+ ["user"]=user,
+ ["password"]=password
+ }
+ http('/login', {
+ postfields = json.encode(post)
+ }, 'http_cb', 5*1000) -- Set a short timeout so user can get more immidiate feedback
+ end
+end
+
+function MatrixServer:initial_sync()
+ BUFFER = w.buffer_new(SCRIPT_NAME, "", "", "closed_matrix_buffer_cb", "")
+ w.buffer_set(BUFFER, "short_name", SCRIPT_NAME)
+ w.buffer_set(BUFFER, "name", SCRIPT_NAME)
+ w.buffer_set(BUFFER, "localvar_set_type", "server")
+ w.buffer_set(BUFFER, "localvar_set_server", SCRIPT_NAME)
+ w.buffer_set(BUFFER, "title", ("Matrix: %s"):format(
+ w.config_get_plugin'homeserver_url'))
+ if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core' then
+ w.buffer_merge(BUFFER, w.buffer_search_main())
+ end
+ w.buffer_set(BUFFER, "display", "auto")
+ local data = urllib.urlencode({
+ access_token = self.access_token,
+ timeout = 1000*POLL_INTERVAL,
+ full_state = 'true',
+ filter = json.encode({ -- timeline filter
+ room = {
+ timeline = {
+ limit = tonumber(w.config_get_plugin('backlog_lines'))
+ }
+ },
+ presence = {
+ not_types = {'*'}, -- dont want presence
+ }
+ })
+ })
+ local extra = 'initial'
+ -- New v2 sync API is slow. Until we can easily ignore archived rooms
+ -- let's increase the timer for the initial login
+ local login_timer = 60*5*1000
+ http('/sync?'..data, nil, 'http_cb', login_timer, extra)
+end
+
+function MatrixServer:post_initial_sync()
+ -- Timer used in cased of errors to restart the polling cycle
+ -- During normal operation the polling should re-invoke itself
+ SERVER.polltimer = w.hook_timer(POLL_INTERVAL*1000, 0, 0, "polltimer_cb", "")
+ if olmstatus then
+ -- timer that checks number of otks available on the server
+ SERVER.otktimer = w.hook_timer(5*60*1000, 0, 0, "otktimer_cb", "")
+ SERVER.olm:query{SERVER.user_id}
+ --SERVER.olm.upload_keys()
+ SERVER.olm:check_server_keycount()
+ end
+end
+
+function MatrixServer:getMessages(room_id, dir, from, limit)
+ if not dir then dir = 'b' end
+ if not from then from = 'END' end
+ if not limit then limit = w.config_get_plugin('backlog_lines') end
+ local data = urllib.urlencode({
+ access_token = self.access_token,
+ dir = dir,
+ from = from,
+ limit = limit,
+ })
+ http(('/rooms/%s/messages?%s')
+ :format(urllib.quote(room_id), data), nil, nil, nil, room_id)
+end
+
+function MatrixServer:Join(room)
+ if not self.connected then
+ --XXX'''
+ return
+ end
+
+ mprint('\tJoining room '..room)
+ room = urllib.quote(room)
+ http('/join/' .. room,
+ {postfields = "access_token="..self.access_token})
+end
+
+function MatrixServer:part(room)
+ if not self.connected then
+ --XXX'''
+ return
+ end
+
+ local id = urllib.quote(room.identifier)
+ local data = urllib.urlencode({
+ access_token= self.access_token,
+ })
+ http(('/rooms/%s/leave?%s'):format(id, data), {postfields = "{}"},
+ 'http_cb', 10000, room.identifier)
+end
+
+function MatrixServer:poll()
+ if self.connected == false then
+ return
+ end
+ if self.poll_lock then
+ return
+ end
+ self.poll_lock = true
+ self.polltime = os.time()
+ local filter = {}
+ if w.config_get_plugin('presence_filter') == 'on' then
+ filter = { -- timeline filter
+ presence = {
+ not_types = {'*'}, -- dont want presence
+ },
+ room = {
+ ephemeral = {
+ not_types = {'*'}, -- dont want read receipt and typing notices
+ }
+ }
+ }
+ end
+ local data = urllib.urlencode({
+ access_token = self.access_token,
+ timeout = 1000*POLL_INTERVAL,
+ full_state = 'false',
+ filter = json.encode(filter),
+ since = self.end_token
+ })
+ http('/sync?'..data, nil, 'http_cb', (POLL_INTERVAL+10)*1000)
+end
+
+function MatrixServer:addRoom(room)
+ -- Just in case, we check for duplicates here
+ if self.rooms[room['room_id']] then
+ return self.rooms[room['room_id']]
+ end
+ local myroom = Room.create(room)
+ myroom:create_buffer()
+ self.rooms[room['room_id']] = myroom
+ return myroom
+end
+
+function MatrixServer:delRoom(room_id)
+ for id, room in pairs(self.rooms) do
+ if id == room_id then
+ mprint('\tLeft room '..room.name)
+ room:destroy()
+ self.rooms[id] = nil
+ break
+ end
+ end
+end
+
+function MatrixServer:SendReadReceipt(room_id, event_id)
+ -- TODO: prevent sending multiple identical read receipts
+ local r_type = 'm.read'
+ local auth = urllib.urlencode{access_token=self.access_token}
+ room_id = urllib.quote(room_id)
+ event_id = urllib.quote(event_id)
+ local url = '/rooms/'..room_id..'/receipt/'..r_type..'/'..event_id..'?'..auth
+ http(url,
+ {customrequest = 'POST'},
+ 'http_cb',
+ 5*1000
+ )
+end
+
+function MatrixServer:Msg(room_id, body, msgtype, url)
+ -- check if there's an outgoing message timer already
+ self:ClearSendTimer()
+
+ if not msgtype then
+ msgtype = 'm.text'
+ end
+
+ if not OUT[room_id] then
+ OUT[room_id] = {}
+ end
+ -- Add message to outgoing queue of messages for this room
+ table.insert(OUT[room_id], {msgtype, body, url})
+
+ self:StartSendTimer()
+end
+
+function MatrixServer:StartSendTimer()
+ local send_delay = 50 -- Wait this long for paste detection
+ self.sendtimer = w.hook_timer(send_delay, 0, 1, "send", "")
+end
+
+function MatrixServer:ClearSendTimer()
+ -- Clear timer if it exists
+ if self.sendtimer then
+ w.unhook(self.sendtimer)
+ end
+ self.sendtimer = nil
+end
+
+function send(cbdata, calls)
+ SERVER:ClearSendTimer()
+ -- Find the room
+ local room
+
+ for id, msgs in pairs(OUT) do
+ -- Clear message
+ OUT[id] = nil
+ local body = {}
+ local htmlbody = {}
+ local msgtype
+ local url
+
+ local ishtml = false
+
+ for _, r in pairs(SERVER.rooms) do
+ if r.identifier == id then
+ room = r
+ break
+ end
+ end
+
+ for _, msg in pairs(msgs) do
+ -- last msgtype will override any other for simplicity's sake
+ msgtype = msg[1]
+ local html = irc_formatting_to_html(msg[2])
+ if html ~= msg[2] then
+ ishtml = true
+ end
+ table.insert(htmlbody, html )
+ table.insert(body, msg[2] )
+ if msg[3] then -- Primarily image upload
+ url = msg[3]
+ end
+ end
+ body = table.concat(body, '\n')
+
+ -- Run IRC modifiers (XXX: maybe run out1 also?
+ body = w.hook_modifier_exec('irc_out1_PRIVMSG', '', body)
+
+ if w.config_get_plugin('local_echo') == 'on' or
+ room.encrypted then
+ -- Generate local echo
+ local color = default_color
+ if msgtype == 'm.text' then
+ --- XXX: no localecho for encrypted messages?
+ local tags = 'notify_none,localecho,no_highlight'
+ if room.encrypted then
+ tags = tags .. ',no_log'
+ color = w.color(w.config_get_plugin(
+ 'encrypted_message_color'))
+ end
+ w.print_date_tags(room.buffer, nil,
+ tags, ("%s\t%s%s"):format(
+ room:formatNick(SERVER.user_id),
+ color,
+ irc_formatting_to_weechat_color(body)
+ )
+ )
+ elseif msgtype == 'm.emote' then
+ local prefix_c = wcolor'weechat.color.chat_prefix_action'
+ local prefix = wconf'weechat.look.prefix_action'
+ local tags = 'notify_none,localecho,irc_action,no_highlight'
+ if room.encrypted then
+ tags = tags .. ',no_log'
+ color = w.color(w.config_get_plugin(
+ 'encrypted_message_color'))
+ end
+ w.print_date_tags(room.buffer, nil,
+ tags, ("%s%s\t%s%s%s %s"):format(
+ prefix_c,
+ prefix,
+ w.color('chat_nick_self'),
+ room.users[SERVER.user_id],
+ color,
+ irc_formatting_to_weechat_color(body)
+ )
+ )
+ end
+ end
+
+ local data = {
+ postfields = {
+ msgtype = msgtype,
+ body = body,
+ url = url,
+ }}
+
+ if ishtml then
+ htmlbody = table.concat(htmlbody, '\n')
+ data.postfields.body = strip_irc_formatting(body)
+ data.postfields.format = 'org.matrix.custom.html'
+ data.postfields.formatted_body = htmlbody
+ end
+
+ local api_event_function = 'm.room.message'
+
+ if olmstatus and room.encrypted then
+ api_event_function = 'm.room.encrypted'
+ local olmd = SERVER.olm
+
+ data.postfields.algorithm = OLM_ALGORITHM
+ data.postfields.sender_key = olmd.device_key
+ data.postfields.ciphertext = {}
+
+ -- Count number of devices we are sending to
+ local recipient_count = 0
+
+ for user_id, _ in pairs(room.users) do
+ for device_id, device_data in pairs(olmd.device_keys[user_id] or {}) do -- FIXME check for missing keys?
+
+ local device_key
+ -- TODO save this better somehow?
+ for key_id, key_data in pairs(device_data.keys) do
+ if key_id:match('^curve25519') then
+ device_key = key_data
+ end
+ end
+ local sessions = olmd:get_sessions(device_key)
+ -- Use the session with the lowest ID
+ -- TODO: figure out how to pick session better?
+ table.sort(sessions)
+ local pickled = next(sessions)
+ if pickled then
+ local session = olm.Session.new()
+ session:unpickle(OLM_KEY, pickled)
+ local session_id = session:session_id()
+ perr(('Session ID: %s, user_id: %s, device_id: %s'):
+ format(session_id, user_id, device_id))
+ local payload = {
+ room_id = room.identifier,
+ ['type'] = "m.room.message",
+ fingerprint = "", -- TODO: Olm:sha256 participants
+ sender_device = olmd.device_id,
+ content = {
+ msgtype = msgtype,
+ body = data.postfields.body or '',
+ url = url
+ }
+ }
+ -- encrypt body
+ local mtype, e_body = session:encrypt(json.encode(payload))
+ local ciphertext = {
+ ["type"] = mtype,
+ body = e_body
+ }
+ data.postfields.ciphertext[device_key] = ciphertext
+ recipient_count = recipient_count + 1
+
+ -- Save session
+ olmd:store_session(device_key, session)
+ session:clear()
+ end
+ end
+ end
+ -- remove cleartext from original msg
+ data.postfields.body = nil
+ data.postfields.formatted_body = nil
+
+ if recipient_count == 0 then
+ perr('Aborted sending of encrypted message: could not find any valid recipients')
+ return
+ end
+ end
+
+ data.postfields = json.encode(data.postfields)
+ data.customrequest = 'PUT'
+
+ http(('/rooms/%s/send/%s/%s?access_token=%s')
+ :format(
+ urllib.quote(id),
+ api_event_function,
+ get_next_transaction_id(),
+ urllib.quote(SERVER.access_token)
+ ),
+ data,
+ nil,
+ nil,
+ id -- send room id to extra
+ )
+ end
+end
+
+function MatrixServer:emote(room_id, body)
+ self:Msg(room_id, body, 'm.emote')
+end
+
+function MatrixServer:notice(room_id, body)
+ self:Msg(room_id, body, 'm.notice')
+end
+
+function MatrixServer:state(room_id, key, data)
+ http(('/rooms/%s/state/%s?access_token=%s')
+ :format(urllib.quote(room_id),
+ urllib.quote(key),
+ urllib.quote(self.access_token)),
+ {customrequest = 'PUT',
+ postfields = json.encode(data),
+ })
+end
+
+function MatrixServer:set_membership(room_id, userid, data)
+ http(('/rooms/%s/state/m.room.member/%s?access_token=%s')
+ :format(urllib.quote(room_id),
+ urllib.quote(userid),
+ urllib.quote(self.access_token)),
+ {customrequest = 'PUT',
+ postfields = json.encode(data),
+ })
+end
+
+function MatrixServer:SendPresence(p, status_msg)
+ -- One of: ["online", "offline", "unavailable", "free_for_chat"]
+ local data = {
+ presence = p,
+ status_msg = status_msg
+ }
+ http(('/presence/%s/status?access_token=%s')
+ :format(
+ urllib.quote(self.user_id),
+ urllib.quote(self.access_token)),
+ {customrequest = 'PUT',
+ postfields = json.encode(data),
+ })
+end
+
+function MatrixServer:SendTypingNotice(room_id)
+ local data = {
+ typing = true,
+ timeout = 4*1000
+ }
+ http(('/rooms/%s/typing/%s?access_token=%s')
+ :format(urllib.quote(room_id),
+ urllib.quote(self.user_id),
+ urllib.quote(self.access_token)),
+ {customrequest = 'PUT',
+ postfields = json.encode(data),
+ })
+end
+
+function MatrixServer:Upload(room_id, filename)
+ local content_type = 'image/jpeg'
+ if filename:match'%.[Pp][nN][gG]$' then
+ content_type = 'image/png'
+ end
+ local url = w.config_get_plugin('homeserver_url') ..
+ ('_matrix/media/r0/upload?access_token=%s')
+ :format( urllib.quote(SERVER.access_token) )
+ w.hook_process_hashtable('curl', {
+ arg1 = '--data-binary', -- no encoding of data
+ arg2 = '@'..filename, -- @means curl will load the filename
+ arg3 = '-XPOST', -- HTTP POST method
+ arg4 = '-H', -- header
+ arg5 = 'Content-Type: '..content_type,
+ arg6 = '-s', -- silent
+ arg7 = url,
+ }, 30*1000, 'upload_cb', room_id)
+end
+
+function MatrixServer:CreateRoom(public, alias, invites)
+ local data = {}
+ if alias then
+ data.room_alias_name = alias
+ end
+ if public then
+ data.visibility = 'public'
+ else
+ data.visibility = 'private'
+ end
+ if invites then
+ data.invite = invites
+ end
+ http(('/createRoom?access_token=%s')
+ :format(urllib.quote(self.access_token)),
+ {customrequest = 'POST',
+ postfields = json.encode(data),
+ })
+end
+
+function MatrixServer:CreateRoomAlias(room_id, alias)
+ local data = {room_id = room_id}
+ alias = urllib.quote(alias)
+ http(('/directory/room/%s?access_token=%s')
+ :format(alias, urllib.quote(self.access_token)),
+ {customrequest = 'PUT',
+ postfields = json.encode(data),
+ })
+end
+
+function MatrixServer:ListRooms(arg)
+ local apipart = ('/publicRooms?access_token=%s'):format(urllib.quote(self.access_token))
+ if arg then
+ local url = 'https://' .. arg .. "/_matrix/client/r0"
+ http(url..apipart)
+ else
+ http(apipart)
+ end
+end
+
+function MatrixServer:Invite(room_id, user_id)
+ local data = {
+ user_id = user_id
+ }
+ http(('/rooms/%s/invite?access_token=%s')
+ :format(urllib.quote(room_id),
+ urllib.quote(self.access_token)),
+ {customrequest = 'POST',
+ postfields = json.encode(data),
+ })
+end
+
+function MatrixServer:Nick(displayname)
+ local data = {
+ displayname = displayname,
+ }
+ http(('/profile/%s/displayname?access_token=%s')
+ :format(
+ urllib.quote(self.user_id),
+ urllib.quote(self.access_token)),
+ {customrequest = 'PUT',
+ postfields = json.encode(data),
+ })
+end
+
+function buffer_input_cb(b, buffer, data)
+ for r_id, room in pairs(SERVER.rooms) do
+ if buffer == room.buffer then
+ SERVER:Msg(r_id, data)
+ break
+ end
+ end
+ return w.WEECHAT_RC_OK
+end
+
+Room = {}
+Room.__index = Room
+Room.create = function(obj)
+ local room = {}
+ setmetatable(room, Room)
+ room.buffer = nil
+ room.identifier = obj['room_id']
+ local _, server = room.identifier:match('^(.*):(.+)$')
+ room.server = server
+ room.member_count = 0
+ -- Cache users for presence/nicklist
+ room.users = {}
+ -- Table of ids currently typing
+ room.typing_ids = {}
+ -- Cache the rooms power levels state
+ room.power_levels = {users={}, users_default=0}
+ -- Encryption status of room
+ room.encrypted = false
+ room.visibility = 'public'
+ room.join_rule = nil
+ room.roomname = nil -- m.room.name
+ room.aliases = nil -- aliases
+ room.canonical_alias = nil
+
+ -- We might not be a member yet
+ local state_events = obj.state or {}
+ for _, state in ipairs(state_events) do
+ if state['type'] == 'm.room.aliases' then
+ local name = state.content.aliases[1]
+ if name then
+ room.name, _ = name:match('(.+):(.+)')
+ end
+ end
+ end
+ if not room.name then
+ room.name = room.identifier
+ end
+ if not room.server then
+ room.server = 'matrix'
+ end
+
+ room.visibility = obj.visibility
+ if not obj['visibility'] then
+ room.visibility = 'public'
+ end
+
+ return room
+end
+
+function Room:SetName(name)
+ if not name or name == '' or name == json.null then
+ return
+ end
+ -- override hierarchy
+ if self.roomname and self.roomname ~= '' then
+ name = self.roomname
+ elseif self.canonical_alias then
+ name = self.canonical_alias
+ local short_name, _ = self.canonical_alias:match('^(.-):(.+)$')
+ if short_name then
+ name = short_name
+ end
+ elseif self.aliases then
+ local alias = self.aliases[1]
+ if name then
+ local _
+ name, _ = alias:match('(.+):(.+)')
+ end
+ else
+ -- NO names. Set dynamic name based on members
+ local new = {}
+ for id, nick in pairs(self.users) do
+ -- Set the name to the other party
+ if id ~= SERVER.user_id then
+ new[#new+1] = nick
+ end
+ end
+ name = table.concat(new, ',')
+ end
+
+ if not name or name == '' or name == json.null then
+ return
+ end
+
+ -- Check for dupe
+ local buffer_name = w.buffer_get_string(self.buffer, 'name')
+ if buffer_name == name then
+ return
+ end
+
+
+ w.buffer_set(self.buffer, "short_name", name)
+ w.buffer_set(self.buffer, "name", name)
+ -- Doesn't work
+ w.buffer_set(self.buffer, "plugin", "matrix")
+ w.buffer_set(self.buffer, "full_name",
+ self.server.."."..name)
+ w.buffer_set(self.buffer, "localvar_set_channel", name)
+end
+
+function Room:Topic(topic)
+ SERVER:state(self.identifier, 'm.room.topic', {topic=topic})
+end
+
+function Room:Name(name)
+ SERVER:state(self.identifier, 'm.room.name', {name=name})
+end
+
+function Room:public()
+ SERVER:state(self.identifier, 'm.room.join_rules', {join_rule='public'})
+end
+
+function Room:Upload(filename)
+ SERVER:Upload(self.identifier, filename)
+end
+
+function Room:Msg(msg)
+ SERVER:Msg(self.identifier, msg)
+end
+
+function Room:emote(msg)
+ SERVER:emote(self.identifier, msg)
+end
+
+function Room:Notice(msg)
+ SERVER:notice(self.identifier, msg)
+end
+
+function Room:SendTypingNotice()
+ SERVER:SendTypingNotice(self.identifier)
+end
+
+function Room:create_buffer()
+ --local buffer = w.buffer_search("", ("%s.%s"):format(self.server, self.name))
+ self.buffer = w.buffer_new(("%s.%s")
+ :format(self.server, self.name), "buffer_input_cb",
+ self.name, "closed_matrix_room_cb", "")
+ -- Needs to correspond with return values from Room:GetNickGroup()
+ -- We will use 5 nick groups:
+ -- 1: Ops
+ -- 2: Half-ops
+ -- 3: Voice
+ -- 4: People with presence
+ -- 5: People without presence
+ self.nicklist_groups = {
+ -- Emulate OPs
+ w.nicklist_add_group(self.buffer,
+ '', "000|o", "weechat.color.nicklist_group", 1),
+ w.nicklist_add_group(self.buffer,
+ '', "001|h", "weechat.color.nicklist_group", 1),
+ -- Emulate half-op
+ w.nicklist_add_group(self.buffer,
+ '', "002|v", "weechat.color.nicklist_group", 1),
+ -- Defined in weechat's irc-nick.h
+ w.nicklist_add_group(self.buffer,
+ '', "998|...", "weechat.color.nicklist_group", 1),
+ w.nicklist_add_group(self.buffer,
+ '', "999|...", "weechat.color.nicklist_group", 1),
+ }
+ w.buffer_set(self.buffer, "nicklist", "1")
+ -- Set to 1 for easier debugging of nick groups
+ w.buffer_set(self.buffer, "nicklist_display_groups", "0")
+ w.buffer_set(self.buffer, "localvar_set_server", self.server)
+ w.buffer_set(self.buffer, "localvar_set_roomid", self.identifier)
+ self:SetName(self.name)
+ if self.membership == 'invite' then
+ self:addNick(self.inviter)
+ if w.config_get_plugin('autojoin_on_invite') ~= 'on' then
+ w.print_date_tags(
+ self.buffer,
+ nil,
+ 'notify_message',
+ ('You have been invited to join room %s by %s. Type /join in this buffer to join.')
+ :format(
+ self.name,
+ self.inviter,
+ self.identifier)
+ )
+ end
+ end
+end
+
+function Room:Freeze()
+ -- Function that saves all the lines in a buffer in a cache to be thawed
+ -- later. Used to redraw buffer when user requests more lines. Since
+ -- WeeChat can only render lines in order this is the workaround
+ local freezer = {}
+ local lines = w.hdata_pointer(w.hdata_get('buffer'), self.buffer, 'own_lines')
+ if lines == '' then return end
+ -- Start at top
+ local line = w.hdata_pointer(w.hdata_get('lines'), lines, 'first_line')
+ if line == '' then return end
+ local hdata_line = w.hdata_get('line')
+ local hdata_line_data = w.hdata_get('line_data')
+ while #line > 0 do
+ local data = w.hdata_pointer(hdata_line, line, 'data')
+ local tags = {}
+ local tag_count = w.hdata_integer(hdata_line_data, data, "tags_count")
+ if tag_count > 0 then
+ for i = 0, tag_count-1 do
+ local tag = w.hdata_string(hdata_line_data, data, i .. "|tags_array")
+ -- Skip notify tags since this is backlog
+ if not tag:match'^notify' then
+ tags[#tags+1] = tag
+ end
+ end
+ end
+ tags[#tags+1] = 'no_log'
+ freezer[#freezer+1] = {
+ time = w.hdata_integer(hdata_line_data, data, 'time'),
+ tags = tags,
+ prefix = w.hdata_string(hdata_line_data, data, 'prefix'),
+ message = w.hdata_string(hdata_line_data, data, 'message'),
+ }
+ -- Move forward since we start at top
+ line = w.hdata_move(hdata_line, line, 1)
+ end
+ self.freezer = freezer
+end
+
+function Room:Thaw()
+ for _,l in ipairs(self.freezer) do
+ w.print_date_tags(
+ self.buffer,
+ l.time,
+ table.concat(l.tags, ','),
+ l.prefix .. '\t' .. l.message
+ )
+ end
+ -- Clear old data
+ self.freezer = nil
+end
+
+function Room:Clear()
+ w.buffer_clear(self.buffer)
+end
+
+function Room:destroy()
+ w.buffer_close(self.buffer)
+end
+
+function Room:_nickListChanged()
+ -- Check the user count, if it's 2 or less then we decide this buffer
+ -- is a "private" one like IRC's query type
+ if self.member_count == 3 then
+ w.buffer_set(self.buffer, "localvar_set_type", 'channel')
+ self.buffer_type = 'channel'
+ elseif self.member_count == 2 then
+ -- At the point where we reach two nicks, set the buffer name to be
+ -- the display name of the other guy that is not our self since it's
+ -- in effect a query, but the matrix protocol doesn't have such
+ -- a concept
+ w.buffer_set(self.buffer, "localvar_set_type", 'private')
+ self.buffer_type = 'query'
+ elseif self.member_count == 1 then
+ if not self.roomname and not self.aliases then
+ -- Set the name to ourselves
+ self:SetName(self.users[SERVER.user_id])
+ end
+ end
+end
+
+function Room:addNick(user_id, displayname)
+ local newnick = false
+ -- Sanitize displaynames a bit
+ if not displayname
+ or displayname == json.null
+ or displayname == ''
+ or displayname:match'^%s+$' then
+ displayname = user_id:match('@(.*):.+')
+ end
+ if not self.users[user_id] then
+ self.member_count = self.member_count + 1
+ newnick = true
+ end
+
+ if self.users[user_id] ~= displayname then
+ self.users[user_id] = displayname
+ end
+
+ local nick_c = self:GetPresenceNickColor(user_id, SERVER.presence[user_id])
+ -- Check if this is ourselves
+ if user_id == SERVER.user_id then
+ w.buffer_set(self.buffer, "highlight_words", displayname)
+ w.buffer_set(self.buffer, "localvar_set_nick", displayname)
+ end
+
+ local ngroup, nprefix, nprefix_color = self:GetNickGroup(user_id)
+ -- Check if nick already exists
+ --local nick_ptr = w.nicklist_search_nick(self.buffer, '', displayname)
+ --if nick_ptr == '' then
+ local nick_ptr = w.nicklist_add_nick(self.buffer,
+ self.nicklist_groups[ngroup],
+ displayname,
+ nick_c, nprefix, nprefix_color, 1)
+ --else
+ -- -- TODO CHANGE nickname here
+ --end
+ if nick_ptr == '' then
+ -- Duplicate nick names :(
+ -- We just add the full id to the nicklist so atleast it will show
+ -- but we should probably assign something new and track the state
+ -- so we can print msgs with non-conflicting nicks too
+ w.nicklist_add_nick(self.buffer,
+ self.nicklist_groups[ngroup],
+ user_id,
+ nick_c, nprefix, nprefix_color, 1)
+ -- Since we can't allow duplicate displaynames, we just use the
+ -- user_id straight up. Maybe we could invent some clever
+ -- scheme here, like user(homeserver), user (2) or something
+ self.users[user_id] = user_id
+ end
+
+ if newnick then -- run this after nick been added so it can be used
+ self:_nickListChanged()
+ end
+
+ return displayname
+end
+
+function Room:GetNickGroup(user_id)
+ -- TODO, cache
+ local ngroup = 5
+ local nprefix = ' '
+ local nprefix_color = ''
+ if self:GetPowerLevel(user_id) >= 100 then
+ ngroup = 1
+ nprefix = '&'
+ nprefix_color = 'lightgreen'
+ if user_id == self.creator then
+ nprefix = '~'
+ nprefix_color = 'lightred'
+ end
+ elseif self:GetPowerLevel(user_id) >= 50 then
+ ngroup = 2
+ nprefix = '@'
+ nprefix_color = 'lightgreen'
+ elseif self:GetPowerLevel(user_id) > 0 then
+ ngroup = 3
+ nprefix = '+'
+ nprefix_color = 'yellow'
+ elseif SERVER.presence[user_id] then
+ -- User has a presence, put him in group3
+ ngroup = 4
+ end
+ return ngroup, nprefix, nprefix_color
+end
+
+function Room:GetPowerLevel(user_id)
+ return tonumber(self.power_levels.users[user_id] or self.power_levels.users_default or 0)
+end
+
+function Room:ClearTyping()
+ for user_id, nick in pairs(self.users) do
+ local _, nprefix, nprefix_color = self:GetNickGroup(user_id)
+ self:UpdateNick(user_id, 'prefix', nprefix)
+ self:UpdateNick(user_id, 'prefix_color', nprefix_color)
+ end
+end
+
+function Room:GetPresenceNickColor(user_id, presence)
+ local nick = self.users[user_id]
+ local nick_c
+ if user_id == SERVER.user_id then
+ -- Always use correct color for self
+ nick_c = 'weechat.color.chat_nick_self'
+ elseif presence == 'online' then
+ nick_c = w.info_get('irc_nick_color_name', nick)
+ elseif presence == 'unavailable' then
+ nick_c = 'weechat.color.nicklist_away'
+ elseif presence == 'offline' then
+ nick_c = 'red'
+ elseif presence == nil then
+ nick_c = 'bar_fg'
+ else
+ dbg{err='unknown presence type',presence=presence}
+ end
+ return nick_c
+end
+
+function Room:UpdatePresence(user_id, presence)
+ if presence == 'typing' then
+ self:UpdateNick(user_id, 'prefix', '!')
+ self:UpdateNick(user_id, 'prefix_color', 'magenta')
+ return
+ end
+ local nick_c = self:GetPresenceNickColor(user_id, presence)
+ self:UpdateNick(user_id, 'color', nick_c)
+end
+
+function Room:UpdateNick(user_id, key, val)
+ local nick = self.users[user_id]
+ if not nick then return end
+ local nick_ptr = w.nicklist_search_nick(self.buffer, '', nick)
+
+ if nick_ptr ~= '' and key and val then
+ -- Check if we need to move the nick into another group
+ local group_ptr = w.nicklist_nick_get_pointer(self.buffer, nick_ptr,
+ 'group')
+ local ngroup, nprefix, nprefix_color = self:GetNickGroup(user_id)
+ if group_ptr ~= self.nicklist_groups[ngroup] then
+ local nick_c = w.nicklist_nick_get_string(self.buffer, nick_ptr,
+ 'color')
+ -- No WeeChat API for changing a nick's group so we will have to
+ -- delete the nick from the old nicklist and add it to the correct
+ -- nicklist group
+ w.nicklist_remove_nick(self.buffer, nick_ptr)
+ -- TODO please check if this call fails, if it does it means the
+ -- WeeChat version is old and has a bug so it can't remove nicks
+ -- and so it needs some workaround
+ nick_ptr = w.nicklist_add_nick(self.buffer,
+ self.nicklist_groups[ngroup],
+ nick,
+ nick_c, nprefix, nprefix_color, 1)
+ end
+ -- Check if we are clearing a typing notice, and don't issue updates
+ -- if we are, because it spams the API so much, including potential
+ -- relay clients
+ if key == 'prefix' and val == ' ' then
+ -- TODO check existing values like + and @ too
+ local prefix = w.nicklist_nick_get_string(self.buffer, nick_ptr,
+ key)
+ if prefix == '!' then
+ w.nicklist_nick_set(self.buffer, nick_ptr, key, val)
+ end
+ elseif key == 'prefix_color' then
+ local prefix_color = w.nicklist_nick_get_string(self.buffer,
+ nick_ptr, key)
+ if prefix_color ~= val then
+ w.nicklist_nick_set(self.buffer, nick_ptr, key, val)
+ end
+ else
+ -- Check if we are actually updating something, so there's less
+ -- updates issued (I think WeeChat sends all changes as nicklist
+ -- diffs to both UI code and to relay clients
+ local existing = w.nicklist_nick_get_string(self.buffer, nick_ptr, key)
+ if val ~= existing then
+ w.nicklist_nick_set(self.buffer, nick_ptr, key, val)
+ end
+ end
+ end
+end
+
+function Room:delNick(id)
+ if self.users[id] then
+ local nick = self.users[id]
+ local nick_ptr = w.nicklist_search_nick(self.buffer, '', nick)
+ if nick_ptr ~= '' then
+ w.nicklist_remove_nick(self.buffer, nick_ptr)
+ end
+ self.users[id] = nil
+ self.member_count = self.member_count - 1
+ self:_nickListChanged()
+ return true
+ end
+end
+
+function Room:UpdateLine(id, message)
+ local lines = w.hdata_pointer(w.hdata_get('buffer'), self.buffer, 'own_lines')
+ if lines == '' then return end
+ local line = w.hdata_pointer(w.hdata_get('lines'), lines, 'last_line')
+ if line == '' then return end
+ local hdata_line = w.hdata_get('line')
+ local hdata_line_data = w.hdata_get('line_data')
+ while #line > 0 do
+ local needsupdate = false
+ local data = w.hdata_pointer(hdata_line, line, 'data')
+ local tags = {}
+ local tag_count = w.hdata_integer(hdata_line_data, data, "tags_count")
+ if tag_count > 0 then
+ for i = 0, tag_count-1 do
+ local tag = w.hdata_string(hdata_line_data, data, i .. "|tags_array")
+ tags[#tags+1] = tag
+ if tag:match(id) then
+ needsupdate = true
+ end
+ end
+ if needsupdate then
+ w.hdata_update(hdata_line_data, data, {
+ prefix = nil,
+ message = message,
+ tags_array = table.concat(tags, ','),
+ })
+ return true
+ end
+ end
+ line = w.hdata_move(hdata_line, line, -1)
+ end
+ return false
+end
+
+function Room:formatNick(user_id)
+ -- Turns a nick name into a weechat-styled nickname. This means giving
+ -- it colors, and proper prefix and suffix
+ local nick = self.users[user_id]
+ if not nick then
+ return user_id
+ end
+ -- Remove nasty white space
+ nick = nick:gsub('[\n\t]', '')
+ local color
+ if user_id == SERVER.user_id then
+ color = w.color('chat_nick_self')
+ else
+ color = w.info_get('irc_nick_color', nick)
+ end
+ local _, nprefix, nprefix_c = self:GetNickGroup(user_id)
+ local prefix = wconf('weechat.look.nick_prefix')
+ local prefix_c = wcolor('weechat.color.chat_nick_prefix')
+ local suffix = wconf('weechat.look.nick_suffix')
+ local suffix_c = wcolor('weechat.color.chat_nick_suffix')
+ local nick_f = prefix_c
+ .. prefix
+ .. wcolor(nprefix_c)
+ .. nprefix
+ .. color
+ .. nick
+ .. suffix_c
+ .. suffix
+ return nick_f
+end
+
+function Room:decryptChunk(chunk)
+ -- vector client doesn't provide this
+ chunk.content.msgtype = 'm.text'
+
+ if not olmstatus then
+ chunk.content.body = 'encrypted message, unable to decrypt'
+ return chunk
+ end
+
+ chunk.content.body = 'encrypted message, unable to decrypt'
+ local device_key = chunk.content.sender_key
+ -- Find our id
+ local ciphertexts = chunk.content.ciphertext
+ local ciphertext
+ if not ciphertexts then
+ chunk.content.body = 'Recieved an encrypted message, but could not find ciphertext array'
+ else
+ ciphertext = ciphertexts[SERVER.olm.device_key]
+ end
+ if not ciphertext then
+ chunk.content.body = 'Recieved an encrypted message, but could not find cipher for ourselves from the sender.'
+ return chunk
+ end
+
+ local session
+ local decrypted
+ local err
+ local found_session = false
+ local sessions = SERVER.olm:get_sessions(device_key)
+ for id, pickle in pairs(sessions) do
+ -- Check if we already successfully decrypted with a sesssion, if that
+ -- is the case we break the loop
+ if decrypted then
+ break
+ end
+ session = olm.Session.new()
+ session:unpickle(OLM_KEY, pickle)
+ local matches_inbound = session:matches_inbound(ciphertext.body)
+ ---if ciphertext.type == 0 and matches_inbound then
+ if matches_inbound then
+ found_session = true
+ end
+ local cleartext
+ cleartext, err = session:decrypt(ciphertext.type, ciphertext.body)
+ if not err then
+ if DEBUG then
+ perr(('olm: Able to decrypt with an existing session %s'):format(session:session_id()))
+ end
+ decrypted = cleartext
+ SERVER.olm:store_session(device_key, session)
+ else
+ chunk.content.body = "Decryption error: "..err
+ if DEBUG then
+ perr(('olm: Unable to decrypt with an existing session: %s. Session-ID: %s'):format(err, session:session_id()))
+ end
+ end
+ session:clear()
+ end
+ if ciphertext.type == 0 and not found_session and not decrypted then
+ session = olm.Session.new()
+ local _
+ _, err = session:create_inbound_from(
+ SERVER.olm.account, device_key, ciphertext.body)
+ if err then
+ session:clear()
+ chunk.content.body = "Decryption error: create inbound "..err
+ return chunk
+ end
+ decrypted, err = session:decrypt(ciphertext.type, ciphertext.body)
+ if err then
+ session:clear()
+ chunk.content.body = "Decryption error: "..err
+ return chunk
+ end
+ -- TODO SERVER.olm.account:remove_one_time_keys(session)
+ local session_id = session:session_id()
+ perr(('Session ID: %s, user_id: %s, device_id: %s'):
+ format(session_id, SERVER.user_id, SERVER.olm.device_id))
+ SERVER.olm:store_session(device_key, session)
+ session:clear()
+ if err then
+ chunk.content.body = "Decryption error: "..err
+ return chunk
+ end
+ end
+
+ if decrypted then
+ local success, payload = pcall(json.decode, decrypted)
+ if not success then
+ chunk.content.body = "Payload error: "..payload
+ return chunk
+ end
+ -- TODO use the room id from payload for security
+ chunk.content.msgtype = payload.content.msgtype
+ -- Style the message so user can tell if it's
+ -- an encrypted message or not
+ local color = w.color(w.config_get_plugin(
+ 'encrypted_message_color'))
+ chunk.content.body = color .. payload.content.body
+ end
+
+ return chunk
+end
+
+-- Parses a chunk of json meant for a room
+function Room:ParseChunk(chunk, backlog, chunktype)
+ local taglist = {}
+ local tag = function(tag)
+ -- Helper function to add tags
+ if type(tag) == 'table' then
+ for _, t in ipairs(tag) do
+ taglist[t] = true
+ end
+ else
+ taglist[tag] = true
+ end
+ end
+ local tags = function()
+ -- Helper for returning taglist for this message
+ local out = {}
+ for k, v in pairs(taglist) do
+ table.insert(out, k)
+ end
+ return table.concat(out, ',')
+ end
+ if not backlog then
+ backlog = false
+ end
+
+ if backlog then
+ tag{'no_highlight','notify_none','no_log'}
+ end
+
+ local is_self = false
+ local was_decrypted = false
+
+ -- Sender of chunk, used to be chunk.user_id, v2 uses chunk.sender
+ local sender = chunk.sender or chunk.user_id
+ -- Check if own message
+ if sender == SERVER.user_id then
+ is_self = true
+ tag{'no_highlight','notify_none'}
+ end
+ -- Add Event ID to each line so can use it later to match on for things
+ -- like redactions and localecho, etc
+ tag{chunk.event_id}
+
+ -- Some messages are missing ts
+ local origin_server_ts = chunk['origin_server_ts'] or 0
+ local time_int = origin_server_ts/1000
+
+ if chunk['type'] == 'm.room.message' or chunk['type'] == 'm.room.encrypted' then
+ if chunk['type'] == 'm.room.encrypted' then
+ tag{'no_log'} -- Don't log encrypted message
+ chunk = self:decryptChunk(chunk)
+ was_decrypted = true
+ end
+
+ if not backlog and not is_self then
+ tag'notify_message'
+ if self.buffer_type == 'query' then
+ tag'notify_private'
+ end
+ end
+
+ local color = default_color
+ local content = chunk['content']
+ local body = content['body']
+
+ if not content['msgtype'] then
+ -- We don't support redactions
+ return
+ end
+
+ -- If it has transaction id, it is from this client.
+ local is_from_this_client = false
+ if chunk.unsigned and chunk.unsigned.transaction_id then
+ is_from_this_client = true
+ end
+
+ -- luacheck: ignore 542
+ if content['msgtype'] == 'm.text' then
+ -- TODO
+ -- Parse HTML here:
+ -- content.format = 'org.matrix.custom.html'
+ -- fontent.formatted_body...
+ elseif content['msgtype'] == 'm.image' then
+ local url = content['url']
+ if type(url) ~= 'string' then
+ url = ''
+ end
+ url = url:gsub('mxc://',
+ w.config_get_plugin('homeserver_url')
+ .. '_matrix/media/v1/download/')
+ -- Synapse homeserver supports arbitrary file endings, so we put
+ -- filename at the end to make it nicer for URL "sniffers" to
+ -- realise it's a image URL
+ body = url .. '/' .. content.body
+ elseif content.msgtype == 'm.file' or content.msgtype == 'm.video' or
+ content.msgtype == 'm.audio' then
+ local url = content['url'] or ''
+ url = url:gsub('mxc://',
+ w.config_get_plugin('homeserver_url')
+ .. '_matrix/media/v1/download/')
+ body = 'File upload: ' ..
+ tostring(content['body'])
+ .. ' ' .. url
+ elseif content['msgtype'] == 'm.notice' then
+ color = wcolor('irc.color.notice')
+ body = content['body']
+ elseif content['msgtype'] == 'm.emote' then
+ local nick_c
+ local nick = self.users[sender] or sender
+ if is_self then
+ nick_c = w.color('chat_nick_self')
+ else
+ nick_c = w.info_get('irc_nick_color', nick)
+ end
+ tag"irc_action"
+ local prefix_c = wcolor'weechat.color.chat_prefix_action'
+ local prefix = wconf'weechat.look.prefix_action'
+ body = ("%s%s %s%s"):format(
+ nick_c, nick, color, content['body']
+ )
+ prefix = prefix_c .. prefix
+ local data = ("%s\t%s"):format(prefix, body)
+ if not backlog and is_self and is_from_this_client and
+ ( w.config_get_plugin('local_echo') == 'on'
+ or was_decrypted -- local echo for encryption
+ )
+ then
+ -- We have already locally echoed this line
+ return
+ else
+ return w.print_date_tags(self.buffer, time_int, tags(), data)
+ end
+ else
+ -- Unknown content type, but if it contains an URL we will print
+ -- URL and body
+ local url = content['url']
+ if url ~= nil then
+ url = url:gsub('mxc://',
+ w.config_get_plugin('homeserver_url')
+ .. '_matrix/media/v1/download/')
+ body = content['body'] .. ' ' .. url
+ end
+ dbg {
+ warning='Warning: unknown/unhandled content type',
+ event=content
+ }
+ end
+ if not backlog and is_self and is_from_this_client
+ -- TODO better check, to work for multiple weechat clients
+ and (
+ w.config_get_plugin('local_echo') == 'on'
+ or was_decrypted -- local echo for encrypted messages
+ )
+ and (-- we don't generate local echo for files and images
+ content.msgtype == 'm.text'
+ )
+ then
+ -- We have already locally echoed this line
+ return
+ end
+ local data = ("%s\t%s%s"):format(
+ self:formatNick(sender),
+ color,
+ body)
+ w.print_date_tags(self.buffer, time_int, tags(), data)
+ elseif chunk['type'] == 'm.room.topic' then
+ local title = chunk['content']['topic']
+ if not title then
+ title = ''
+ end
+ w.buffer_set(self.buffer, "title", title)
+ local color = wcolor("irc.color.topic_new")
+ local nick = self.users[sender] or sender
+ local data = ('--\t%s%s has changed the topic to "%s%s%s"'):format(
+ nick,
+ default_color,
+ color,
+ title,
+ default_color
+ )
+ w.print_date_tags(self.buffer, chunk.origin_server_ts, tags(),
+ data)
+ elseif chunk['type'] == 'm.room.name' then
+ local name = chunk['content']['name']
+ if name ~= '' or name ~= json.null then
+ self.roomname = name
+ self:SetName(name)
+ end
+ elseif chunk['type'] == 'm.room.member' then
+ if chunk['content']['membership'] == 'join' then
+ tag"irc_join"
+ --- FIXME shouldn't be neccessary adding all the time
+ local name = self.users[sender] or self:addNick(sender, chunk.content.displayname)
+ if not name or name == json.null or name == '' then
+ name = sender
+ end
+ -- Check if the chunk has prev_content or not
+ -- if there is prev_content there wasn't a join but a nick change
+ -- or duplicate join
+ local prev_content = chunk.unsigned and chunk.unsigned.prev_content
+ if prev_content
+ and prev_content.membership == 'join'
+ and chunktype == 'messages' then
+ local oldnick = prev_content.displayname
+ if not oldnick or oldnick == json.null then
+ oldnick = sender
+ else
+ if oldnick == name then
+ -- Maybe they changed their avatar or something else
+ -- that we don't care about (or multiple joins)
+ return
+ end
+ if not backlog then
+ self:delNick(sender)
+ self:addNick(sender, chunk.content.displayname)
+ end
+ end
+ local pcolor = wcolor'weechat.color.chat_prefix_network'
+ tag'irc_nick'
+ local data = ('%s--\t%s%s%s is now known as %s%s'):format(
+ pcolor,
+ w.info_get('irc_nick_color', oldnick),
+ oldnick,
+ default_color,
+ w.info_get('irc_nick_color', name),
+ name)
+ w.print_date_tags(self.buffer, time_int, tags(), data)
+ elseif chunktype == 'messages' then
+ tag"irc_smart_filter"
+ local data = ('%s%s\t%s%s%s (%s%s%s) joined the room.'):format(
+ wcolor('weechat.color.chat_prefix_join'),
+ wconf('weechat.look.prefix_join'),
+ w.info_get('irc_nick_color', name),
+ name,
+ wcolor('irc.color.message_join'),
+ wcolor'weechat.color.chat_host',
+ sender,
+ wcolor('irc.color.message_join')
+ )
+ w.print_date_tags(self.buffer, time_int, tags(), data)
+ -- if this is an encrypted room, also download key
+ if olmstatus and self.encrypted then
+ SERVER.olm:query{sender}
+ end
+ end
+ elseif chunk['content']['membership'] == 'leave' then
+ if chunktype == 'messages' then
+ local nick = self.users[chunk.state_key] or sender
+ local prev = chunk.unsigned.prev_content
+ if (prev and
+ prev.displayname and
+ prev.displayname ~= json.null) then
+ nick = prev.displayname
+ end
+ if sender ~= chunk.state_key then -- Kick
+ tag{"irc_quit","irc_kick","irc_smart_filter"}
+ local reason = chunk.content.reason or ''
+ local sender_nick = self.users[chunk.sender]
+ local data = ('%s%s\t%s%s%s has kicked %s%s%s (%s).'):format(
+ wcolor('weechat.color.chat_prefix_quit'),
+ wconf('weechat.look.prefix_quit'),
+ w.info_get('irc_nick_color', sender_nick),
+ sender_nick,
+ wcolor('irc.color.message_quit'),
+ w.info_get('irc_nick_color', nick),
+ nick,
+ default_color,
+ reason
+ )
+ w.print_date_tags(self.buffer, time_int, tags(), data)
+ else
+ tag{"irc_quit","irc_smart_filter"}
+ local data = ('%s%s\t%s%s%s left the room.'):format(
+ wcolor('weechat.color.chat_prefix_quit'),
+ wconf('weechat.look.prefix_quit'),
+ w.info_get('irc_nick_color', nick),
+ nick,
+ wcolor('irc.color.message_quit')
+ )
+ w.print_date_tags(self.buffer, time_int, tags(), data)
+ end
+ end
+ self:delNick(chunk.state_key)
+ elseif chunk['content']['membership'] == 'invite' then
+ -- Check if we were the one being invited
+ if chunk.state_key == SERVER.user_id and (
+ (not backlog and chunktype == 'messages') or
+ chunktype == 'states') then
+ --self:addNick(sender)
+ if w.config_get_plugin('autojoin_on_invite') == 'on' then
+ SERVER:Join(self.identifier)
+ mprint(('%s invited you'):format(sender))
+ else
+ mprint(('You have been invited to join room %s by %s. Type /join %s to join.')
+ :format(
+ self.name,
+ sender,
+ self.identifier))
+ end
+ end
+ if chunktype == 'messages' then
+ tag"irc_invite"
+ local prefix_c = wcolor'weechat.color.chat_prefix_action'
+ local prefix = wconf'weechat.look.prefix_action'
+ local data = ("%s%s\t%s invited %s to join"):format(
+ prefix_c,
+ prefix,
+ self.users[sender] or sender,
+ self.users[chunk.state_key] or chunk.state_key
+ )
+ w.print_date_tags(self.buffer, time_int, tags(), data)
+ end
+ elseif chunk['content']['membership'] == 'ban' then
+ if chunktype == 'messages' then
+ tag"irc_ban"
+ local prefix_c = wcolor'weechat.color.chat_prefix_action'
+ local prefix = wconf'weechat.look.prefix_action'
+ local data = ("%s%s\t%s banned %s"):format(
+ prefix_c,
+ prefix,
+ self.users[sender] or sender,
+ self.users[chunk.state_key] or chunk.state_key
+ )
+ w.print_date_tags(self.buffer, time_int, tags(), data)
+ end
+ else
+ dbg{err= 'unknown membership type in ParseChunk', chunk= chunk}
+ end
+ -- if it's backlog this is done at the end from the caller place
+ if not backlog then
+ -- Run SetName on each member change in case we need to update room name
+ self:SetName(self.identifier)
+ end
+ elseif chunk['type'] == 'm.room.create' then
+ self.creator = chunk.content.creator
+ elseif chunk['type'] == 'm.room.power_levels' then
+ if chunk.content.users then
+ self.power_levels = chunk.content
+ for user_id, lvl in pairs(self.power_levels.users) do
+ -- TODO
+ -- calculate changes here and generate message lines
+ -- describing the change
+ end
+ for user_id, lvl in pairs(self.power_levels.users) do
+ local _, nprefix, nprefix_color = self:GetNickGroup(user_id)
+ self:UpdateNick(user_id, 'prefix', nprefix)
+ self:UpdateNick(user_id, 'prefix_color', nprefix_color)
+ end
+ end
+ elseif chunk['type'] == 'm.room.join_rules' then
+ -- TODO: parse join_rules events --
+ self.join_rules = chunk.content
+ elseif chunk['type'] == 'm.typing' then
+ -- Store the typing ids in a table that the bar item can use
+ local typing_ids = {}
+ for _, id in ipairs(chunk.content.user_ids) do
+ self:UpdatePresence(id, 'typing')
+ typing_ids[#typing_ids+1] = self.users[id]
+ end
+ self.typing_ids = typing_ids
+ w.bar_item_update('matrix_typing_notice')
+ elseif chunk['type'] == 'm.presence' then
+ SERVER:UpdatePresence(chunk)
+ elseif chunk['type'] == 'm.room.aliases' then
+ -- Use first alias, weechat doesn't really support multiple aliases
+ self.aliases = chunk.content.aliases
+ self:SetName(chunk.content.aliases[1])
+ elseif chunk['type'] == 'm.room.canonical_alias' then
+ self.canonical_alias = chunk.content.alias
+ self:SetName(self.canonical_alias)
+ elseif chunk['type'] == 'm.room.redaction' then
+ local redact_id = chunk.redacts
+ --perr('Redacting message ' .. redact_id)
+ local result = self:UpdateLine(redact_id, w.color'darkgray'..'(redacted)')
+ if not result and not backlog then
+ -- backlog doesn't send original message
+ perr 'Could not find message to redact :('
+ end
+ elseif chunk['type'] == 'm.room.history_visibility' then
+ self.history_visibility = chunk.content.history_visibility
+ -- luacheck: ignore 542
+ elseif chunk['type'] == 'm.receipt' then
+ -- TODO: figure out if we can do something sensible with read receipts
+ else
+ if DEBUG then
+ perr(('Unknown event type %s%s%s in room %s%s%s'):format(
+ w.color'bold',
+ chunk.type,
+ default_color,
+ w.color'bold',
+ self.name,
+ default_color))
+ dbg{chunk=chunk}
+ end
+ end
+end
+
+function Room:Op(nick)
+ for id, name in pairs(self.users) do
+ if name == nick then
+ -- patch the locally cached power levels
+ self.power_levels.users[id] = 99
+ SERVER:state(self.identifier, 'm.room.power_levels',
+ self.power_levels)
+ break
+ end
+ end
+end
+
+function Room:Voice(nick)
+ for id, name in pairs(self.users) do
+ if name == nick then
+ -- patch the locally cached power levels
+ self.power_levels.users[id] = 25
+ SERVER:state(self.identifier, 'm.room.power_levels',
+ self.power_levels)
+ break
+ end
+ end
+end
+
+function Room:Devoice(nick)
+ for id, name in pairs(self.users) do
+ if name == nick then
+ -- patch the locally cached power levels
+ self.power_levels.users[id] = 0
+ SERVER:state(self.identifier, 'm.room.power_levels',
+ self.power_levels)
+ break
+ end
+ end
+end
+
+function Room:Deop(nick)
+ for id, name in pairs(self.users) do
+ if name == nick then
+ -- patch the locally cached power levels
+ self.power_levels.users[id] = 0
+ SERVER:state(self.identifier, 'm.room.power_levels',
+ self.power_levels)
+ break
+ end
+ end
+end
+
+function Room:Kick(nick, reason)
+ for id, name in pairs(self.users) do
+ if name == nick then
+ local data = {
+ membership = 'leave',
+ reason = 'Kicked by '..SERVER.user_id
+ }
+ SERVER:set_membership(self.identifier, id, data)
+ break
+ end
+ end
+end
+
+function Room:Whois(nick)
+ for id, name in pairs(self.users) do
+ if name == nick then
+ local pcolor = wcolor'weechat.color.chat_prefix_network'
+ local data = ('%s--\t%s%s%s has user id %s%s'):format(
+ pcolor,
+ w.info_get('irc_nick_color', nick),
+ nick,
+ default_color,
+ w.info_get('irc_nick_color', id),
+ id)
+ w.print_date_tags(self.buffer, nil, 'notify_message', data)
+ local pdata = ('%s--\t%s%s%s has presence %s%s'):format(
+ pcolor,
+ w.info_get('irc_nick_color', nick),
+ nick,
+ default_color,
+ pcolor,
+ SERVER.presence[id] or 'offline')
+ w.print_date_tags(self.buffer, nil, 'notify_message', pdata)
+ -- TODO support printing status_msg field in presence data here
+ break
+ end
+ end
+end
+
+function Room:Invite(id)
+ SERVER:Invite(self.identifier, id)
+end
+
+function Room:Encrypt()
+ self.encrypted = true
+ -- Download keys for all members
+ self:Download_keys()
+ -- Create sessions
+ -- Pickle.
+ -- Save
+end
+function Room:Download_keys()
+ for id, name in pairs(self.users) do
+ -- TODO enable batch downloading of keys here when synapse can handle it
+ SERVER.olm:query({id})
+ end
+end
+
+function Room:MarkAsRead()
+ -- Get event id from tag of last line in buffer
+ local lines = w.hdata_pointer(w.hdata_get('buffer'), self.buffer, 'own_lines')
+ if lines == '' then return end
+ local line = w.hdata_pointer(w.hdata_get('lines'), lines, 'last_line')
+ if line == '' then return end
+ local hdata_line = w.hdata_get('line')
+ local hdata_line_data = w.hdata_get('line_data')
+ local data = w.hdata_pointer(hdata_line, line, 'data')
+ local tag_count = w.hdata_integer(hdata_line_data, data, "tags_count")
+ if tag_count > 0 then
+ for i = 0, tag_count-1 do
+ local tag = w.hdata_string(hdata_line_data, data, i .. "|tags_array")
+ -- Event ids are like $142533663810152bfUKc:matrix.org
+ if tag:match'^%$.*:' then
+ SERVER:SendReadReceipt(self.identifier, tag)
+ break
+ end
+ end
+ end
+end
+
+function poll(a, b)
+ SERVER:poll()
+ return w.WEECHAT_RC_OK
+end
+
+function polltimer_cb(a, b)
+ local now = os.time()
+ if (now - SERVER.polltime) > POLL_INTERVAL+10 then
+ -- Release the poll lock
+ SERVER.poll_lock = false
+ SERVER:poll()
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function otktimer_cb(a, b)
+ SERVER.olm:check_server_keycount()
+ return w.WEECHAT_RC_OK
+end
+
+function cleartyping(a, b)
+ for id, room in pairs(SERVER.rooms) do
+ room:ClearTyping()
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function join_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if current_buffer == BUFFER or room then
+ local _, alias = split_args(args)
+ if not alias then
+ -- To support running /join on a invited room without args
+ SERVER:Join(room.identifier)
+ else
+ SERVER:Join(alias)
+ end
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function part_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ SERVER:part(room)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function leave_command_cb(data, current_buffer, args)
+ return part_command_cb(data, current_buffer, args)
+end
+
+function me_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, message = split_args(args)
+ room:emote(message or '')
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function topic_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, topic = split_args(args)
+ room:Topic(topic)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function upload_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, upload = split_args(args)
+ room:Upload(upload)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function query_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, query = split_args(args)
+ for id, displayname in pairs(room.users) do
+ if displayname == query then
+ -- Create a new room and invite the guy
+ SERVER:CreateRoom(false, nil, {id})
+ return w.WEECHAT_RC_OK_EAT
+ end
+ end
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function create_command_cb(data, current_buffer, args)
+ local command, arg = split_args(args)
+ local room = SERVER:findRoom(current_buffer)
+ if (room or current_buffer == BUFFER) and command == '/create' then
+ if arg then
+ -- Room names are supposed to be without # and homeserver, so
+ -- we try to help the user out here
+ local alias = arg:match'#?(.*):?'
+ -- Create a non-public room with argument as alias
+ SERVER:CreateRoom(false, alias, nil)
+ else
+ mprint 'Use /create room-name'
+ end
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function createalias_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, alias = split_args(args)
+ SERVER:CreateRoomAlias(room.identifier, alias)
+ return w.WEECHAT_RC_OK_EAT
+ elseif current_buffer == BUFFER then
+ mprint 'Use /createalias #alias:homeserver.domain from a room'
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function invite_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, invitee = split_args(args)
+ room:Invite(invitee)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function list_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room or current_buffer == BUFFER then
+ local _, target = split_args(args)
+ SERVER:ListRooms(target)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function op_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, target = split_args(args)
+ room:Op(target)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function voice_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, target = split_args(args)
+ room:Voice(target)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function devoice_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, target = split_args(args)
+ room:Devoice(target)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+function deop_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, target = split_args(args)
+ room:Deop(target)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function kick_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, target = split_args(args)
+ room:Kick(target)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function nick_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room or current_buffer == BUFFER then
+ local _, nick = split_args(args)
+ SERVER:Nick(nick)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function whois_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, nick = split_args(args)
+ room:Whois(nick)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function notice_command_cb(data, current_buffer, args)
+ -- TODO sending from matrix buffer given a room name
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, msg = split_args(args)
+ room:Notice(msg)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function msg_command_cb(data, current_buffer, args)
+ local _, msgmask = split_args(args)
+ local mask, msg = split_args(msgmask)
+ local room
+ -- WeeChat uses * as a mask for current buffer
+ if mask == '*' then
+ room = SERVER:findRoom(current_buffer)
+ else
+ for id, r in pairs(SERVER.rooms) do
+ -- Send /msg to a ID
+ if id == mask then
+ room = r
+ break
+ elseif mask == r.name then
+ room = r
+ break
+ end
+ end
+ end
+
+ if room then
+ room:Msg(msg)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function encrypt_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, arg = split_args(args)
+ if arg == 'on' then
+ mprint('Enabling encryption for outgoing messages in room ' .. tostring(room.name))
+ room:Encrypt()
+ elseif arg == 'off' then
+ mprint('Disabling encryption for outgoing messages in room ' .. tostring(room.name))
+ room.encrypted = false
+ else
+ w.print(current_buffer, 'Use /encrypt on or /encrypt off to turn encryption on or off')
+ end
+ return w.WEECHAT_RC_OK_EAT
+ else
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function public_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ mprint('Marking room as public: ' .. tostring(room.name))
+ room:public()
+ return w.WEECHAT_RC_OK_EAT
+ else
+ mprint('Run command from a room')
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function names_command_cb(cbdata, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local nrcolor = function(nr)
+ return wcolor'weechat.color.chat_channel'
+ .. tostring(nr)
+ .. default_color
+ end
+ local buffer_name = nrcolor(w.buffer_get_string(room.buffer, 'name'))
+ local delim_c = wcolor'weechat.color.chat_delimiters'
+ local tags = 'no_highlight,no_log,irc_names'
+ local pcolor = wcolor'weechat.color.chat_prefix_network'
+ local ngroups = {}
+ local nicks = {}
+ for id, name in pairs(room.users) do
+ local ncolor
+ if id == SERVER.user_id then
+ ncolor = w.color('chat_nick_self')
+ else
+ ncolor = w.info_get('irc_nick_color', name)
+ end
+ local ngroup, nprefix, nprefix_color = room:GetNickGroup(id)
+ if nprefix == ' ' then nprefix = '' end
+ nicks[#nicks+1] = ('%s%s%s%s'):format(
+ w.color(nprefix_color),
+ nprefix,
+ ncolor,
+ name
+ )
+ if not ngroups[ngroup] then
+ ngroups[ngroup] = 0
+ end
+ ngroups[ngroup] = ngroups[ngroup] + 1
+ end
+ local line1 = ('%s--\tNicks %s: %s[%s%s]'):format(
+ pcolor,
+ buffer_name,
+ delim_c,
+ table.concat(nicks, ' '),
+ delim_c
+ )
+ w.print_date_tags(room.buffer, 0, tags, line1)
+ local line2 = (
+ '%s--\tChannel %s: %s nicks %s(%s%s ops, %s voice, %s normals%s)'
+ ):format(
+ pcolor,
+ buffer_name,
+ nrcolor(room.member_count),
+ delim_c,
+ default_color,
+ nrcolor((ngroups[1] or 0) + (ngroups[2] or 0)),
+ nrcolor(ngroups[3] or 0),
+ nrcolor((ngroups[4] or 0) + (ngroups[5] or 0)),
+ delim_c
+ )
+ w.print_date_tags(room.buffer, 0, tags, line2)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ perr('Could not find room')
+ return w.WEECHAT_RC_OK
+ end
+end
+
+function more_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ SERVER:getMessages(room.identifier, 'b', room.prev_batch, 120)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ perr('/more Could not find room')
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function roominfo_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ dbg{room=room}
+ return w.WEECHAT_RC_OK_EAT
+ else
+ perr('/roominfo Could not find room')
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function name_command_cb(data, current_buffer, args)
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ local _, name = split_args(args)
+ room:Name(name)
+ return w.WEECHAT_RC_OK_EAT
+ else
+ perr('/name Could not find room')
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function closed_matrix_buffer_cb(data, buffer)
+ BUFFER = nil
+ return w.WEECHAT_RC_OK
+end
+
+function closed_matrix_room_cb(data, buffer)
+ -- WeeChat closed our room
+ local room = SERVER:findRoom(buffer)
+ if room then
+ room.buffer = nil
+ perr('Room got closed: '..room.name)
+ SERVER.rooms[room.identifier] = nil
+ return w.WEECHAT_RC_OK
+ end
+ return w.WEECHAT_RC_ERR
+end
+
+function typing_notification_cb(signal, sig_type, data)
+ -- Ignore commands
+ if data:match'^/' then
+ return w.WEECHAT_RC_OK
+ end
+ -- Is this signal coming from a matrix buffer?
+ local room = SERVER:findRoom(data)
+ if room then
+ local input = w.buffer_get_string(data, "input")
+ -- Start sending when it reaches > 4 and doesn't start with command
+ if #input > 4 and not input:match'^/' then
+ local now = os.time()
+ -- Generate typing events every 4th second
+ if SERVER.typing_time + 4 < now then
+ SERVER.typing_time = now
+ room:SendTypingNotice()
+ end
+ end
+ end
+
+ return w.WEECHAT_RC_OK
+end
+
+function buffer_switch_cb(signal, sig_type, data)
+ -- Update bar item
+ w.bar_item_update('matrix_typing_notice')
+ local current_buffer = w.current_buffer()
+ local room = SERVER:findRoom(current_buffer)
+ if room then
+ room:MarkAsRead()
+ end
+ return w.WEECHAT_RC_OK
+end
+
+function typing_bar_item_cb(data, buffer, args)
+ local current_buffer = w.current_buffer()
+ local room = SERVER:findRoom(current_buffer)
+ if not room then return '' end
+ local typing_ids = table.concat(room.typing_ids, ' ')
+ if #typing_ids > 0 then
+ return "Typing: ".. typing_ids
+ end
+ return ''
+end
+
+if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "matrix_unload", "UTF-8") then
+ -- Save WeeChat version to a global so other functionality can see it
+ local version = w.info_get('version_number', '') or 0
+ WEECHAT_VERSION = tonumber(version)
+ local settings = {
+ homeserver_url= {'https://matrix.org/', 'Full URL including port to your homeserver (including trailing slash) or use default matrix.org'},
+ user= {'', 'Your homeserver username'},
+ password= {'', 'Your homeserver password'},
+ backlog_lines= {'120', 'Number of lines to fetch from backlog upon connecting'},
+ presence_filter = {'off', 'Filter presence messages and ephemeral events (for performance)'},
+ autojoin_on_invite = {'on', 'Automatically join rooms you are invited to'},
+ typing_notices = {'on', 'Send typing notices when you type'},
+ local_echo = {'on', 'Print lines locally instead of waiting for return from server'},
+ debug = {'off', 'Print a lot of extra information to help with finding bugs and other problems.'},
+ encrypted_message_color = {'lightgreen', 'Print encrypted mesages with this color'},
+ --olm_secret = {'', 'Password used to secure olm stores'},
+ }
+ -- set default settings
+ for option, value in pairs(settings) do
+ if w.config_is_set_plugin(option) ~= 1 then
+ w.config_set_plugin(option, value[1])
+ end
+ if WEECHAT_VERSION >= 0x00030500 then
+ w.config_set_desc_plugin(option, ('%s (default: "%s")'):format(
+ value[2], value[1]))
+ end
+ end
+ errprefix = wconf'weechat.look.prefix_error'
+ errprefix_c = wcolor'weechat.color.chat_prefix_error'
+ HOMEDIR = w.info_get('weechat_dir', '') .. '/'
+ local commands = {
+ 'join', 'part', 'leave', 'me', 'topic', 'upload', 'query', 'list',
+ 'op', 'voice', 'deop', 'devoice', 'kick', 'create', 'createalias', 'invite', 'nick',
+ 'whois', 'notice', 'msg', 'encrypt', 'public', 'names', 'more',
+ 'roominfo', 'name'
+ }
+ for _, c in pairs(commands) do
+ w.hook_command_run('/'..c, c..'_command_cb', '')
+ end
+
+ if w.config_get_plugin('typing_notices') == 'on' then
+ w.hook_signal('input_text_changed', "typing_notification_cb", '')
+ end
+
+ if w.config_get_plugin('debug') == 'on' then
+ DEBUG = true
+ end
+
+ w.hook_config('plugins.var.lua.matrix.debug', 'configuration_changed_cb', '')
+
+ local cmds = {'help', 'connect', 'debug', 'msg'}
+ w.hook_command(SCRIPT_COMMAND, 'Plugin for matrix.org chat protocol',
+ '[command] [command options]',
+ 'Commands:\n' ..table.concat(cmds, '\n') ..
+ '\nUse /matrix help [command] to find out more\n' ..
+ '\nSupported slash commands (i.e. /commands):\n' ..
+ table.concat(commands, ', '),
+ -- Completions
+ table.concat(cmds, '|'),
+ 'matrix_command_cb', '')
+
+ w.hook_command_run('/away -all*', 'matrix_away_command_run_cb', '')
+ SERVER = MatrixServer.create()
+
+ if WEECHAT_VERSION < 0x01040000 then
+ perr(SCRIPT_NAME .. ': Please upgrade your WeeChat before using this script. Using this script on older WeeChat versions may lead to crashes. Many bugs have been fixed in newer versions of WeeChat.')
+ perr(SCRIPT_NAME .. ': Refusing to automatically connect you. If you insist, type /'..SCRIPT_COMMAND..' connect, and do not act surprised if it crashes :-)')
+ else
+ SERVER:connect()
+ end
+
+ w.hook_signal('buffer_switch', "buffer_switch_cb", "")
+ w.bar_item_new('matrix_typing_notice', 'typing_bar_item_cb', '')
+end