6 Commits

Author SHA1 Message Date
soutade ca11b0e175 Version 15 2024-08-31 16:36:07 +02:00
soutade d0e8f0cde5 Code review from JustPerfection 2024-08-31 13:58:43 +02:00
soutade 66d79cc804 Update for Gnome Shell 45+ 2024-08-29 20:46:21 +02:00
soutade 3ed3394a33 Update metadata.json with Gnome Shell 44 2023-09-25 19:14:35 +02:00
soutade 7cd668af14 Count number of threads instead of unread messages in gmail example 2022-11-20 21:01:41 +01:00
soutade 5690307c63 Update Gmail access with OAuth 2 2022-11-20 20:00:34 +01:00
5 changed files with 141 additions and 76 deletions
+85
View File
@@ -0,0 +1,85 @@
#
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Setup : https://developers.google.com/gmail/api/quickstart/python
# Need to have credentials.json were you call main script
#
# File imported from https://github.com/googleworkspace/python-samples/blob/main/gmail/quickstart/quickstart.py
#
from __future__ import print_function
import os.path
import json
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
creds = None
def _initCreds():
global creds
if creds: return
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('./token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
data = {}
data['refresh_token'] = creds.refresh_token
data['client_id'] = creds.client_id
data['client_secret'] = creds.client_secret
data['token_uri'] = creds.token_uri
data['id_token'] = creds.id_token
token.write(json.dumps(data))
def getUnreadMails():
"""
Get number of unread threads (that may contain multiple messages)
"""
_initCreds()
service = build('gmail', 'v1', credentials=creds)
pageToken = ''
threads = set()
while True:
results = service.users().messages().list(userId='me', labelIds=['UNREAD'],\
includeSpamTrash=False, pageToken=pageToken)\
.execute()
if not 'messages' in results.keys(): continue
threads = threads.union(set([k['threadId'] for k in results['messages']]))
# Loop over all result pages (100 results per page by default)
pageToken = results.get('nextPageToken', '')
if not pageToken: break
return len(threads)
+2 -12
View File
@@ -23,11 +23,11 @@ import time
import requests import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
import xml.dom.minidom import xml.dom.minidom
import getpass
from threading import Thread from threading import Thread
from signal import signal, SIGINT from signal import signal, SIGINT
import sys import sys
from genericmonitor import * from genericmonitor import *
from gmail import getUnreadMails
PURPLE_CONV_UPDATE_UNSEEN = 4 PURPLE_CONV_UPDATE_UNSEEN = 4
PURPLE_MESSAGE_SEND = 0 PURPLE_MESSAGE_SEND = 0
@@ -59,22 +59,16 @@ class PidginConversation:
class EventThread(Thread,GenericMonitor): class EventThread(Thread,GenericMonitor):
SLEEP_TIME = 30 SLEEP_TIME = 30
MAIL_ADDRESS='XXX@gmail.com'
def stop(self): def stop(self):
self._stopLoop = True self._stopLoop = True
self.stopMainLoop() self.stopMainLoop()
def _getMail(self): def _getMail(self):
address = "https://mail.google.com/mail/feed/atom"
auth = HTTPBasicAuth(self.MAIL_ADDRESS, self._mail_password)
req = requests.get(address, auth=auth)
text = '' text = ''
style = '' style = ''
if req.status_code == requests.codes.ok:
dom = xml.dom.minidom.parseString(req.text)
try: try:
nb_messages = int(dom.getElementsByTagName('fullcount')[0].firstChild.nodeValue) nb_messages = getUnreadMails()
if nb_messages == 1: if nb_messages == 1:
text = '1 msg' text = '1 msg'
elif nb_messages > 1: elif nb_messages > 1:
@@ -82,8 +76,6 @@ class EventThread(Thread,GenericMonitor):
style = 'color:white' style = 'color:white'
except Exception as e: except Exception as e:
text = str(e) text = str(e)
else:
text = 'Mail error %d' % (req.status_code)
self.mailWidget.setText(text) self.mailWidget.setText(text)
self.mailWidget.setStyle(style) self.mailWidget.setStyle(style)
@@ -103,8 +95,6 @@ class EventThread(Thread,GenericMonitor):
self.add_signal_receiver(self.pidginMessageWrote, 'WroteChatMsg', 'im.pidgin.purple.PurpleInterface') self.add_signal_receiver(self.pidginMessageWrote, 'WroteChatMsg', 'im.pidgin.purple.PurpleInterface')
self.add_signal_receiver(self.pidginConversationUpdated, 'ConversationUpdated', 'im.pidgin.purple.PurpleInterface') self.add_signal_receiver(self.pidginConversationUpdated, 'ConversationUpdated', 'im.pidgin.purple.PurpleInterface')
self._mail_password = getpass.getpass('Enter password for address %s: ' % (self.MAIL_ADDRESS))
self.mailWidget = GenericMonitorTextWidget('') self.mailWidget = GenericMonitorTextWidget('')
mailItem = GenericMonitorItem('mail', [self.mailWidget]) mailItem = GenericMonitorItem('mail', [self.mailWidget])
self.mailGroup = GenericMonitorGroup('Mail', [mailItem]) self.mailGroup = GenericMonitorGroup('Mail', [mailItem])
+2 -5
View File
@@ -43,15 +43,12 @@ class PicturePopup(GenericMonitor):
self.runMainLoop() self.runMainLoop()
def display_next_img(self): def display_next_img(self):
filedata = urllib.request.urlopen('https://source.unsplash.com/random') filedata = urllib.request.urlopen('https://picsum.photos/500/500')
# Get redirected URL without parameters
url = filedata.url.split('?')[0]
filedata = urllib.request.urlopen(url + '?fit=max&width=500&height=500')
datatowrite = filedata.read() datatowrite = filedata.read()
with open('/tmp/cat2.jpg', 'wb') as f: with open('/tmp/cat2.jpg', 'wb') as f:
f.write(datatowrite) f.write(datatowrite)
widget = GenericMonitorTextWidget('#%d' % self.imgs_idx, 'color:purple') widget = GenericMonitorTextWidget('#%d' % self.imgs_idx, 'color:purple')
url_widget = GenericMonitorTextWidget(url, 'color:white;font-weight:bold', signals={'on-click':'signal'}) # No name here url_widget = GenericMonitorTextWidget('random_pic', 'color:white;font-weight:bold', signals={'on-click':'signal'}) # No name here
picture_widget = GenericMonitorPictureWidget('/tmp/cat2.jpg', name='NestedWidget', signals={'on-click':'signal'}) picture_widget = GenericMonitorPictureWidget('/tmp/cat2.jpg', name='NestedWidget', signals={'on-click':'signal'})
popup = GenericMonitorPopup([url_widget, picture_widget]) popup = GenericMonitorPopup([url_widget, picture_widget])
signals = { signals = {
+36 -42
View File
@@ -29,17 +29,17 @@
https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/panel.js https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/panel.js
*/ */
const St = imports.gi.St; import * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
const Gio = imports.gi.Gio; import St from 'gi://St';
const GLib = imports.gi.GLib import Gio from 'gi://Gio';
const Main = imports.ui.main; import GLib from 'gi://GLib';
const Mainloop = imports.mainloop; import Clutter from 'gi://Clutter';
const Clutter = imports.gi.Clutter; import GObject from 'gi://GObject';
const PanelMenu = imports.ui.panelMenu; import Pixbuf from 'gi://GdkPixbuf';
const PopupMenu = imports.ui.popupMenu; import Cogl from 'gi://Cogl';
const GObject = imports.gi.GObject; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
const Pixbuf = imports.gi.GdkPixbuf; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
const Cogl = imports.gi.Cogl; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
function hashGet(hash, key, defaultValue) { function hashGet(hash, key, defaultValue) {
@@ -50,7 +50,7 @@ function hashGet(hash, key, defaultValue) {
} }
function log(message) { function log(message) {
global.log('[GenericMontior]', message); console.error('[GenericMontior]', message);
} }
@@ -79,7 +79,7 @@ class SignalMgt {
this.onScroll = hashGet(item, 'on-scroll', ''); this.onScroll = hashGet(item, 'on-scroll', '');
} }
destructor() { destroy() {
for(let widgetIdx in this.widgets) for(let widgetIdx in this.widgets)
this.disconnectWidgetSignals(this.widgets[widgetIdx]); this.disconnectWidgetSignals(this.widgets[widgetIdx]);
for(let timeoutIdx in this.timeouts) for(let timeoutIdx in this.timeouts)
@@ -189,7 +189,8 @@ class SignalMgt {
this.button = event.get_button(); this.button = event.get_button();
this.nbClicks = 1; this.nbClicks = 1;
let sourceId = Mainloop.timeout_add(this.dbus.ClutterSettings['double-click-time'], let sourceId = GLib.timeout_add(GLib.G_PRIORITY_DEFAULT,
this.dbus.ClutterSettings['double-click-time'],
this._doClickCallback.bind(this)); this._doClickCallback.bind(this));
this.timeouts.push(sourceId); this.timeouts.push(sourceId);
} }
@@ -228,7 +229,7 @@ class MyPopupMenuItem extends PopupMenu.PopupBaseMenuItem {
this.box.set_vertical(true); this.box.set_vertical(true);
for (let widgetIndex in widgets) for (let widgetIndex in widgets)
this.box.add(widgets[widgetIndex]); this.box.add_child(widgets[widgetIndex]);
this.add_child(this.box); this.add_child(this.box);
} }
@@ -393,6 +394,7 @@ class MonitorWidget extends PanelMenu.Button {
destroy() { destroy() {
this.menu.close(); this.menu.close();
this.signalManager.destroy();
super.destroy(); super.destroy();
} }
@@ -555,14 +557,12 @@ class MonitorWidget extends PanelMenu.Button {
// From https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/interfaces.js // From https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/interfaces.js
// loads a xml file into an in-memory string // loads a xml file into an in-memory string
function loadInterfaceXml(filename) { function loadInterfaceXml(extension, filename) {
const extension = imports.misc.extensionUtils.getCurrentExtension();
const interfacesDir = extension.dir.get_child('.'); const interfacesDir = extension.dir.get_child('.');
const file = interfacesDir.get_child(filename); const file = interfacesDir.get_child(filename);
let [result, contents] = imports.gi.GLib.file_get_contents(file.get_path()); let [result, contents] = GLib.file_get_contents(file.get_path());
if (result) { if (result) {
// HACK: The "" + trick is important as hell because file_get_contents returns // HACK: The "" + trick is important as hell because file_get_contents returns
@@ -571,7 +571,10 @@ function loadInterfaceXml(filename) {
// is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough, // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
// will spit out a TypeError soon). // will spit out a TypeError soon).
if (contents instanceof Uint8Array) if (contents instanceof Uint8Array)
contents = imports.byteArray.toString(contents); {
const decoder = new TextDecoder();
contents = decoder.decode(contents);
}
const res = `<node>${contents}</node>`; const res = `<node>${contents}</node>`;
return res; return res;
} else { } else {
@@ -580,11 +583,11 @@ function loadInterfaceXml(filename) {
} }
class GenericMonitorDBUS { class GenericMonitorDBUS {
constructor() { constructor(extension) {
this.monitor_groups = {}; this.monitor_groups = {};
this.actor_clicked = {}; this.actor_clicked = {};
this.ClutterSettings = Clutter.Settings.get_default(); this.ClutterSettings = Clutter.Settings.get_default();
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(loadInterfaceXml('dbus.xml'), this); this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(loadInterfaceXml(extension, 'dbus.xml'), this);
this._dbusImpl.export(Gio.DBus.session, '/com/soutade/GenericMonitor'); this._dbusImpl.export(Gio.DBus.session, '/com/soutade/GenericMonitor');
this._dbusImpl.emit_signal('onActivate', null); this._dbusImpl.emit_signal('onActivate', null);
} }
@@ -787,7 +790,7 @@ class GenericMonitorDBUS {
monitorWidget.togglePopup(); monitorWidget.togglePopup();
} }
destructor() { destroy() {
this._dbusImpl.emit_signal('onDeactivate', null); this._dbusImpl.emit_signal('onDeactivate', null);
for (let groupIndex in this.monitor_groups) { for (let groupIndex in this.monitor_groups) {
const group = this.monitor_groups[groupIndex]; const group = this.monitor_groups[groupIndex];
@@ -799,30 +802,21 @@ class GenericMonitorDBUS {
} }
} }
class Extension { export default class GenericMonitorExtension extends Extension.Extension {
constructor(...args) {
super(...args);
this.textDBusService = null;
}
enable() { enable() {
this.textDBusService = new GenericMonitorDBUS(); this.textDBusService = new GenericMonitorDBUS(this);
} }
disable() { disable() {
this.textDBusService.destructor(); if (this.textDBusService !== null) {
this.textDBusService.destroy();
delete this.textDBusService; delete this.textDBusService;
this.textDBusService = null;
} }
} }
let extension = null;
function init() {
}
function enable() {
extension = new Extension();
extension.enable();
}
function disable() {
if (extension) {
extension.disable();
extension = null;
}
} }
+5 -6
View File
@@ -2,12 +2,11 @@
"uuid": "generic-monitor@gnome-shell-extensions", "uuid": "generic-monitor@gnome-shell-extensions",
"name": "Generic Monitor", "name": "Generic Monitor",
"description": "Display text & icon on systray using DBUS", "description": "Display text & icon on systray using DBUS",
"version": "12", "version": "15",
"shell-version": [ "shell-version": [
"43", "47",
"42.3", "46",
"42", "45"
"41"
], ],
"url": "http://indefero.soutade.fr/p/genericmonitor" "url": "https://forge.soutade.fr/soutade/GnomeShellGenericMonitor"
} }