]> git.somenet.org - pub/jan/mattermost.git/blob - mw_vowi/main.py
[somebot] requirements.txt for all modules.
[pub/jan/mattermost.git] / mw_vowi / main.py
1 #!/usr/bin/env python3
2 # Someone's Mattermost scripts.
3 #  Copyright (c) 2016-2020 by Someone <someone@somenet.org> (aka. Jan Vales <jan@jvales.net>)
4 #  published under MIT-License
5 #
6
7 import json
8 import os
9 import pprint
10 import random
11 import re
12 import signal
13 import sys
14 import time
15 import traceback
16 import mwclient
17 import mattermost
18
19 import config
20
21
22 ########
23 # Util #
24 ########
25 # global regexes
26 REXtuwien = re.compile(r'^TU Wien\:(.*) \w\w,?\w?\w? \(.*\).*$', re.IGNORECASE)
27 REXdaten = re.compile(r'==\s*Daten\s*==', re.MULTILINE|re.DOTALL|re.IGNORECASE)
28 REXwdata_mmchan = re.compile(r'\*??\s*\{\{mattermost-channel\|([a-zA-Z0-9_ -]+)\}\}\s*\n{0,2}', re.IGNORECASE)
29
30 def get_smw_properties(subject, sobj_dict={'sobj': {}}):
31     ret = dict()
32     for prop in subject:
33         if prop['property'] not in ret:
34             ret[prop['property']] = []
35         for dataitem in prop['dataitem']:
36             if dataitem['type'] == 9 and dataitem['item'] in sobj_dict['sobj']:
37                 ret[prop['property']] += [sobj_dict['sobj'][dataitem['item']]]
38             else:
39                 ret[prop['property']] += [dataitem['item']]
40     return ret
41
42 def query_smw_properties(site, pagename):
43     res = site.api('browsebysubject', subject=pagename)
44
45     # gather all sub objects first
46     ret = {'sobj': {}}
47     if 'sobj' in res['query']:
48         for sobj in res['query']['sobj']:
49             ret['sobj'][sobj['subject']] = get_smw_properties(sobj['data'])
50
51     ret = get_smw_properties(res['query']['data'], ret)
52
53     if '_ASK' in ret:
54         del ret['_ASK']
55     return ret
56
57
58 def mw_pagename_to_mm_chan_mapping(pagename):
59     try:
60         mmchdisplayname = REXtuwien.search(pagename).group(1)
61         vowiurlname = mmchdisplayname.replace(' ', '_')
62         mmchname = mmchdisplayname.lower().replace('ä', 'ae').replace('ö', 'oe').replace('ü', 'ue')
63         mmchdisplayname = mmchdisplayname[:63]
64
65         mmchname = re.sub(r'[^a-zA-Z0-9_]', r'-', mmchname)
66         mmchname = re.sub(r'-+', r'-', mmchname)
67         mmchname = mmchname[:63]
68         mmchname = mmchname.strip('-')
69
70         # now even group-channels (len == 40) do weird shit. Thanks MM :/
71         # Heisen-Issue: for some reason MM uses sometimes the channel name as id if len == 26 gives and when switching between teams.
72         if len(mmchname) == 54 or len(mmchname) == 40 or len(mmchname) == 26:
73             mmchname = mmchname+'0'
74
75         return (mmchname, (mmchname, mmchdisplayname, vowiurlname, pagename))
76     except:
77         print('Failed to process pagemapping for: '+pagename)
78         return ("", ("", "", "", ""))
79
80 #########
81 # /Util #
82 #########
83
84
85
86 def create_update_spam_mm_channel(mws, mma, basepage_name, changes_to_notify_about=dict()):
87     print('\ncreate_update_spam_mm_channel(): https://vowi.fsinf.at/wiki/'+basepage_name.replace(' ', '_').replace(')', '%29'))
88
89     # really purge cache :/
90     sp = mws.Pages[basepage_name]
91     sp.purge()
92
93     if not sp.exists or sp.redirect:
94         print('Page was deleted or moved. ignoring.')
95         return
96
97     # get smw data of basepage
98     semantic_data = query_smw_properties(mws, sp.name)
99     sp_text_daten = sp_text_daten_orig = sp.text(section=1)
100     if REXdaten.match(sp_text_daten) is None:
101         raise Exception('"== Daten ==" not section=1')
102
103     # abort if veraltet
104     if 'Ist_veraltet' in semantic_data and semantic_data['Ist_veraltet'][0] == 't':
105         print('Page is veraltet. ignoring.')
106         return
107
108     # generate metadata
109     basepage_name_mapping = mw_pagename_to_mm_chan_mapping(basepage_name)
110     metadata = dict()
111     metadata['display_name'] = basepage_name_mapping[1][1]
112     metadata['purpose'] = 'LVA(s) im VoWi: https://vowi.fsinf.at/LVA/'+basepage_name_mapping[1][2]+'?mm'
113     metadata['header'] = 'Links: [LVA(s) im VoWi](https://vowi.fsinf.at/LVA/'+basepage_name_mapping[1][2]+'?mm)'
114
115     if 'Hat_Kurs-ID' in semantic_data:
116         metadata['header'] += ' - [LVA in TISS](https://tiss.tuwien.ac.at/course/courseDetails.xhtml?courseNr='+semantic_data['Hat_Kurs-ID'][0]+')'
117     if 'Hat_Homepage' in semantic_data:
118         metadata['header'] += ' - [LVA-HP]('+semantic_data['Hat_Homepage'][0]+')'
119
120     # create channel, if not exist
121     mmchan = mma.get_channel_by_name(config.mm_autochannels_team, basepage_name_mapping[0])
122     if 'status_code' in mmchan and mmchan['status_code'] == 404:
123         print(mma.create_channel(config.mm_autochannels_team, basepage_name_mapping[0], metadata['display_name'], metadata['purpose'], metadata['header']))
124
125         # really get channel
126         mmchan = mma.get_channel_by_name(config.mm_autochannels_team, basepage_name_mapping[0])
127
128
129     # update wiki:data-section. always.
130     sp_text_daten = re.sub(REXwdata_mmchan, '', sp_text_daten)
131     sp_text_daten = sp_text_daten.strip()
132     sp_text_daten += '\n{{mattermost-channel|'+basepage_name_mapping[0]+'}}'
133     if sp_text_daten != sp_text_daten_orig:
134         print('Updating MW page: '+str(sp.save(sp_text_daten, section=1, summary='mw_vowi')))
135
136
137     # update channel
138     if 'type' in mmchan and mmchan['type'] == 'O' and ((sp_text_daten != sp_text_daten_orig) or (len(changes_to_notify_about) > 0)):
139         mma.patch_channel(mmchan['id'], metadata)
140
141     # spam channel if there is anything to spam about
142     if len(changes_to_notify_about) > 0:
143         msg = '``BOT-AUTODELETE-SLOW``\nFolgende Seiten wurden in den letzten 24 Stunden im VoWi editiert:\n'
144         for v in changes_to_notify_about.values():
145             msg += '\nhttps://vowi.fsinf.at/wiki/'+v['title'].replace(' ', '_')
146         msg += '\n\n``footer``\n'+str(random.choice(config.mm_footerlist))
147         print('Posting to channel:' +str(mma.create_post(mmchan['id'], msg)))
148
149
150
151 def process_recent_vowi_changes(mws, mma):
152     lastdata = dict()
153     try:
154         with open('data/last.json') as fh:
155             lastdata = json.load(fh)
156     except:
157         print('Failed to load last.json')
158
159     changes = dict()
160     if 'failed' in lastdata:
161         changes = lastdata['failed']
162
163     if 'delme' in lastdata:
164         del lastdata['delme']
165
166     if 'timestamp' in lastdata:
167         rc = mws.recentchanges(start=lastdata['timestamp'], dir='newer', namespace=3000, show='!bot|!redirect|!minor', toponly=True)
168     else:
169         rc = mws.recentchanges(dir='newer', namespace=3000, show='!bot|!redirect|!minor', toponly=False)
170         lastdata['timestamp'] = 0
171     lastdata['last_timestamp'] = lastdata['timestamp']
172
173     if 'rcid' not in lastdata:
174         lastdata['rcid'] = 0
175
176     for c in rc:
177         if c['rcid'] == lastdata['rcid']:
178             continue
179
180         basepage = c['title'].split('/', 1)[0]
181         if basepage not in changes:
182             changes[basepage] = dict()
183         changes[basepage][c['title']] = c
184         changes[basepage][c['title']]['timestamp'] = int(time.strftime('%Y%m%d%H%M%S', changes[basepage][c['title']]['timestamp']))
185
186         if changes[basepage][c['title']]['timestamp'] > lastdata['timestamp']:
187             lastdata['timestamp'] = changes[basepage][c['title']]['timestamp']
188         if changes[basepage][c['title']]['rcid'] > lastdata['rcid']:
189             lastdata['rcid'] = changes[basepage][c['title']]['rcid']
190
191     lastdata['failed'] = changes
192
193     # process all pending changes
194     for ch in list(changes):
195         semantic_data = query_smw_properties(mws, ch)
196
197         # never mind, its outdated anyway ...
198         if 'Ist_veraltet' in semantic_data and semantic_data['Ist_veraltet'][0] == 't':
199             del lastdata['failed'][ch]
200             continue
201
202         try:
203             print("trying: "+ch)
204             create_update_spam_mm_channel(mws, mma, ch, changes[ch])
205             del lastdata['failed'][ch]
206         except Exception as ex:
207             print('Exception, skipping: '+ch+' - '+str(sys.exc_info()))
208             traceback.print_exc()
209
210         with open('data/last.json', 'w') as fh:
211             json.dump(lastdata, fh)
212
213     if len(lastdata['failed']) != 0:
214         print('Some changes failed to get posted!')
215         pprint.pprint(lastdata)
216
217
218
219 def process_all_LVAs(mws, mma):
220     for p in mws.ask('[[TU Wien:+]][[Ist veraltet::0]]'):
221         create_update_spam_mm_channel(mws, mma, ''.join(p))
222
223
224
225 # remove "has mm-channel" info from outdated lva pages.
226 def process_outdated_LVAs(mws):
227     for p in mws.ask('[[Hat Mattermost-Channel::+]][[Ist veraltet::1]]'):
228         sp = mws.Pages[(''.join(p.keys()))]
229         print('\nProcessing: https://vowi.fsinf.at/wiki/'+sp.name.replace(' ', '_').replace(')', '%29'))
230
231         sp_text = sp.text(section=1)
232
233         sp_text = re.sub(REXwdata_mmchan, '', sp_text)
234
235         print(sp.save(sp_text, section=1, summary='mw_vowi'))
236         sp.purge()
237
238
239
240 def process_outdated_MMChannels(mws, mma, spam_channel=False):
241     # get a dict of all lva-pages that have a mm-channel saved and map them to all the possible channel-names.
242     # Legitimate (not outdated LVAs)
243     mw_page_names_with_mm_channelname_mappings = dict([mw_pagename_to_mm_chan_mapping(c) for c in [''.join(p) for p in mws.ask('[[Hat Mattermost-Channel::+]]')]])
244
245     # All existing mm channels
246     mm_all_channels_with_infos = {mmchan['name']:mmchan for mmchan in mma.get_team_channels(config.mm_autochannels_team)}
247
248     # outdated candidates.
249     diff_channel_names = set(mm_all_channels_with_infos.keys()) - set(mw_page_names_with_mm_channelname_mappings.keys())
250     diff_channel_names.discard('town-square')
251     diff_channel_names.discard('off-topic')
252
253     # get replacements
254     # dict of all outdated LVA-pages.
255     mw_outdated_page_names_mappings = dict([mw_pagename_to_mm_chan_mapping(c) for c in [''.join(p) for p in mws.ask('[[TU Wien:+]][[Kategorie:LVAs]][[Ist veraltet::1]]')]])
256 # TODO: seems b0rked.
257 #    mw_lva_ersetzt_durch = dict()
258 #    for p in mws.ask('[[Ersetzt durch::+]]|?Ersetzt durch'):
259 #        for k,v in p.items():
260 #            mw_lva_ersetzt_durch[k] = v['printouts']['Ersetzt durch'][0]
261 #
262 #    mw_lva_wirklich_ersetzt_durch = mw_lva_ersetzt_durch.copy()
263 #    pprint.pprint(mw_lva_wirklich_ersetzt_durch)
264 #    return
265 #    for k,v in mw_lva_ersetzt_durch.items():
266 #         ersatz = k
267 #         while ersatz in mw_lva_ersetzt_durch:
268 #             ersatz = mw_lva_ersetzt_durch[ersatz]
269 #         mw_lva_wirklich_ersetzt_durch[k] = ersatz
270 #
271
272     # notify admins
273     msg = '``BOT-AUTODELETE-SLOW``\n#### ``Likely outdated MM channels found``\nClick ``Next...`` to load all channels, to make these links work\nCheck via vowi if there is really no not-outdated LVA-page.\n'
274     msg_inconsistent = ''
275     real_diff_channel_names = diff_channel_names.copy()
276     for d in diff_channel_names:
277         try:
278             msg += ' + ~'+d+' :arrow_right: https://vowi.fsinf.at/wiki/Spezial:FlexiblePrefix/'+mw_outdated_page_names_mappings[d][2]+'\n'
279         except:
280             msg_inconsistent += ' + ~'+d+' :warning: ``No known VoWi URL - SWM-Consistency issue?``\n'
281             real_diff_channel_names.discard(d)
282
283     if len(diff_channel_names) == 0:
284         msg = '``BOT-AUTODELETE-SLOW``\n#### `` No outdated MM channels found`` :)\n'
285
286     if len(msg_inconsistent) > 0:
287         msg += '\n\nThese ('+str(len(diff_channel_names)-len(real_diff_channel_names))+') are likely SMW inconsistencies and were skipped:\n'+msg_inconsistent
288         msg += 'If there are many of these, consider running ``SemanticMediaWiki/maintenance/rebuildData.php``'
289
290     # make real_ the real deal
291     diff_channel_names = real_diff_channel_names
292     del real_diff_channel_names
293
294     print('Posting '+str(len(msg))+' chars to channel:' +str(mma.create_post(config.mm_admin_channel, msg)))
295
296
297     # notify channels
298     if not spam_channel:
299         return
300
301     for mmchan in diff_mmchan_names:
302         print('notifying: '+mmchan)
303         metadata = dict()
304         try:
305             metadata['purpose'] = 'Outdated? Dieser LVA-Channel wurde als outdated markiert, da alle seine VoWi-LVAs outdated sind. LVA(s) im VoWi: https://vowi.fsinf.at/LVA/'+mw_mmchans_full_all[mmchan][2].replace(' ', '_')+'?mm'
306             metadata['header'] = ':warning: Outdated? :warning: Links: [Outdated LVA(s) im VoWi](https://vowi.fsinf.at/LVA/'+mw_mmchans_full_all[mmchan][2].replace(' ', '_')+'?mm)'
307         except:
308             metadata['purpose'] = 'Outdated? Dieser LVA-Channel wurde als outdated markiert, da alle seine VoWi-LVAs outdated sind. Sollte dies ein Fehler sein, korrigiere bitte die entsprechende LVA im VoWi.'
309             metadata['header'] = ':warning: Outdated? :warning: Dieser LVA-Channel wurde als outdated markiert, da alle seine VoWi-LVAs outdated sind.'
310
311         # update channel info.
312         print(mma.patch_channel(mm_mmchans_full[mmchan]['id'], metadata))
313
314         # post "something changed" info
315         msg = ('``BOT-AUTODELETE-SLOW``\n### Dieser LVA-Channel wurde als outdated markiert, da alle seine VoWi-LVAs outdated sind.\n'
316                'Sollte dies ein Fehler sein, korrigiere bitte die entsprechende LVA-Seite im VoWi.\n'
317                '``Falls die LVA von einer neuen Person übernommen wurde und sich dabei die Durchführung grundlegend geändert hat, oder dies zu erwarten ist, lege bitte eine neue LVA-Seite an.``\n\n'
318                '## :warning: :warning: :warning: :warning: :warning: :warning: :warning:\n\n'
319                )
320
321         try:
322             msg += ':arrow_right: Die Nachfolge-LVA laut VoWi ist: https://vowi.fsinf.at/LVA/'+mw_lva_ersetzt_durch[mmchan].replace(' ', '_')+' \n'
323         except:
324             msg += ':warning: Eine Nachfolge-LVA ist nicht bekannt, im VoWi nicht vermerkt, oder konnte nicht ermittelt werden :(\n'
325         try:
326             msg += ':arrow_right: Der Nachfolge-LVA-MM-Channel dürfte sein: ~'+mw_pagename_to_mm_chan_mapping(mw_lva_ersetzt_durch[mmchan])[0]
327         except:
328             msg += ':warning: Ein Nachfolge-LVA-MM-Channel konnte nicht ermittelt werden :('
329
330         print('Posting to channel:' +str(mma.create_post(mm_mmchans_full[mmchan]['id'], msg)))
331
332
333
334 if __name__ == '__main__':
335     def signal_handler(signal, frame):
336         print('SIG received. exitting!')
337         os._exit(0)
338     signal.signal(signal.SIGINT, signal_handler)
339
340     mws = mwclient.Site(config.mw_name, path='/', retry_timeout=120)
341     mws.login(config.mw_user, config.mw_user_pw)
342     mma = mattermost.MMApi(config.mm_api_url)
343     mma.login(config.mm_user, config.mm_user_pw)
344
345     # Use recent changes to create, update and spam channels.
346     if len(sys.argv) == 1:
347         process_recent_vowi_changes(mws, mma)
348
349     # maint stuff
350     # "full" run modes - We cant run the whole script at once, due to out db-backup, likely killing our run mid-excecution. :(
351     # "new" should actually not really be needed, as it should be covered by incremental changes.
352     if len(sys.argv) > 1 and sys.argv[1] == 'all-pages':
353         print(sys.argv[1])
354         process_all_LVAs(mws, mma)
355
356     if len(sys.argv) > 1 and sys.argv[1] == 'outdated-pages':
357         print(sys.argv[1])
358         process_outdated_LVAs(mws)
359
360     if len(sys.argv) > 1 and sys.argv[1] == 'outdated-chans':
361         print(sys.argv[1])
362         process_outdated_MMChannels(mws, mma, False)
363
364     if len(sys.argv) > 1 and sys.argv[1] == 'outdated-chans-spam':
365         print(sys.argv[1])
366         process_outdated_MMChannels(mws, mma, True)
367
368     # logout
369     mma.revoke_user_session()